Compare commits
6 commits
f8f514e6dd
...
21e0f11c41
| Author | SHA1 | Date | |
|---|---|---|---|
| 21e0f11c41 | |||
| 3747782394 | |||
| 9678860e0a | |||
| f8ea67dbe3 | |||
| df88d2a954 | |||
| a6e556b616 |
11 changed files with 461 additions and 6 deletions
12
Project.toml
12
Project.toml
|
|
@ -1,7 +1,17 @@
|
|||
name = "CualerID"
|
||||
uuid = "a4892dd5-cee5-4429-912e-1922c40e7f9b"
|
||||
authors = ["Thomas A. Christensen II <25492070+MillironX@users.noreply.github.com> and contributors"]
|
||||
version = "0.0.1"
|
||||
authors = ["Thomas A. Christensen II <25492070+MillironX@users.noreply.github.com> and contributors"]
|
||||
|
||||
[deps]
|
||||
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
|
||||
FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
|
||||
[compat]
|
||||
ArgParse = "1.2.0"
|
||||
FilePathsBase = "0.9.24"
|
||||
UUIDs = "1.11.0"
|
||||
julia = "1.12"
|
||||
|
||||
[apps.cualer-id]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# CualerID
|
||||
|
||||
[](https://github.com/invenia/BlueStyle)
|
||||
[](https://github.com/JuliaTesting/Aqua.jl)
|
||||
|
|
|
|||
106
src/Base30.jl
Normal file
106
src/Base30.jl
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
BASE30_ALPHABET::Vector{UInt8}
|
||||
|
||||
An alphabet designed for unambiguous representation of values in either upper-
|
||||
or lower-case. The alphabet was formed by taking the numbers 0-9 and the Latin
|
||||
alphabet and removing characters that are considered easy to confuse. Excluded
|
||||
characters are
|
||||
|
||||
- ``I`` => easily confused for lowercase ``L`` or the digit ``1``
|
||||
- ``L`` => easily confused for uppercase ``I`` or the digit ``1``
|
||||
- ``O`` => easily confused for the digit ``0``
|
||||
- ``Q`` => easily confused with uppercase ``O`` or lowercase ``g``
|
||||
- ``V`` => easily confused with ``U``
|
||||
|
||||
This array contains the ASCII index of the characters included in the base30
|
||||
alphabet starting with 0 at position `BASE30_ALPHABET[1]`. Use `String()` to
|
||||
convert an array of these indices to a base30 string.
|
||||
"""
|
||||
const BASE30_ALPHABET = UInt8.([
|
||||
48:57..., #0-9
|
||||
65:72..., #A-H
|
||||
74:75..., #J-K
|
||||
77:78..., #M-N
|
||||
80, #P
|
||||
83:85..., #S-U
|
||||
87:90..., #W-Z
|
||||
])
|
||||
|
||||
"""
|
||||
rebase(v::Integer, base::Integer)
|
||||
|
||||
Rebase `v` into the base system of `base`. `v` can be any integer type, and can
|
||||
be represented in base 10 (decimal) or base 16 (hexadecimal) based on Julia's
|
||||
treatment of that integer type. Returns a `Vector{UInt8}` containing the
|
||||
**indices** of each digit in the rebased number. Since Julia is 1-indexed, this
|
||||
means that `rebase(0, base)[1]` actually returns a value of `UInt8[1]`, since
|
||||
the first index of a numeral system's alphabet is assumed to be its ``0`` value.
|
||||
|
||||
# Examples
|
||||
|
||||
```jldoctest
|
||||
julia> rebase(0, 10)
|
||||
1-element Vector{UInt8}:
|
||||
0x01
|
||||
|
||||
julia> # Two plus two is ten... IN BASE FOUR! I'M FINE!
|
||||
|
||||
julia> rebase(2 + 2, 4)
|
||||
2-element Vector{UInt8}:
|
||||
0x02
|
||||
0x01
|
||||
|
||||
julia> base_4_alphabet = ['0', '1', '2', '3'];
|
||||
|
||||
julia> String([base_4_alphabet[i] for i in rebase(2 + 2, 4)])
|
||||
"10"
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
This method is not tested on negative integers and may produce incorrect
|
||||
output when passed negative numbers
|
||||
"""
|
||||
function rebase(v::Integer, base::Integer)
|
||||
remainder = UInt8((v % base) + 1)
|
||||
quotient = div(v, base)
|
||||
|
||||
if quotient == 0
|
||||
return [remainder]
|
||||
else
|
||||
return [rebase(quotient, base)..., remainder]
|
||||
end #if
|
||||
|
||||
end #function
|
||||
|
||||
"""
|
||||
base30encode(v::Integer)
|
||||
base30encode(v::UUID)
|
||||
|
||||
Creates a base30 representation of `v`. If `v` is a `UUID`, then the `UInt128`
|
||||
value of the `UUID` is passed directly into the method.
|
||||
|
||||
# Examples
|
||||
|
||||
```jldoctest; filter = r"[0-9A-Z]{26}"
|
||||
julia> base30encode(30)
|
||||
"10"
|
||||
|
||||
julia> using UUIDs
|
||||
|
||||
julia> base30encode(UUIDs.uuid1())
|
||||
"7ZK00JECBDF2H7GNBXN59C6S9S"
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Just like [`rebase`](@ref), this method is not tested on negative integers
|
||||
and may produce incorrect output when passed negative numbers
|
||||
"""
|
||||
function base30encode(v::Integer)
|
||||
return String([BASE30_ALPHABET[i] for i in rebase(v, 30)])
|
||||
end #function
|
||||
|
||||
function base30encode(v::UUID)
|
||||
return base30encode(v.value)
|
||||
end #function
|
||||
173
src/CualerID.jl
173
src/CualerID.jl
|
|
@ -1,5 +1,174 @@
|
|||
module CualerID
|
||||
|
||||
# Write your package code here.
|
||||
using ArgParse: ArgParse, ArgParseSettings, parse_args, parse_item, @add_arg_table!
|
||||
using FilePathsBase: AbstractPath, Path, exists, isfile
|
||||
using UUIDs: UUID
|
||||
|
||||
end
|
||||
include("Base30.jl")
|
||||
include("ResultCodeOption.jl")
|
||||
|
||||
export ResultCode
|
||||
export ResultCodeOption
|
||||
export base30encode
|
||||
export has_duplicate
|
||||
export has_fixed
|
||||
export has_not_fixable
|
||||
export has_valid
|
||||
export rebase
|
||||
|
||||
function _create_arg_table()
|
||||
s = ArgParseSettings(autofix_names = true)
|
||||
|
||||
#! format: off
|
||||
@add_arg_table! s begin
|
||||
"create"
|
||||
help = "Create CualerID sample ids or barcode labels"
|
||||
action = :command
|
||||
"fix"
|
||||
help = "Compare a set of possibly invalid IDs against a correct set of IDs to identify errors in the IDs"
|
||||
action = :command
|
||||
"convert"
|
||||
help = "Convert CualID sample ids to CualerID sample ids or vice-versa"
|
||||
action = :command
|
||||
end #add_arg_table
|
||||
|
||||
@add_arg_table! s["create"] begin
|
||||
"ids"
|
||||
help = "Generate sample ids"
|
||||
action = :command
|
||||
"labels"
|
||||
help = "Generate a pdf of barcodes (NOT YET IMPLEMENTED)"
|
||||
action = :command
|
||||
end #add_arg_table
|
||||
|
||||
@add_arg_table! s["create"]["ids"] begin
|
||||
"--length", "-l"
|
||||
help = "The length of the printed sample ids"
|
||||
arg_type = UInt64
|
||||
default = UInt64(5)
|
||||
"--fail-threshold", "-f"
|
||||
help = "NOT IMPLEMENTED: The rate at which similar ids will fail the process"
|
||||
arg_type = Float64
|
||||
range_tester = x -> x >= 0 && x <= 1.0
|
||||
default = 0.99
|
||||
"number_of_ids"
|
||||
help = "The number of sample ids to print"
|
||||
arg_type = UInt64
|
||||
range_tester = x -> x >= 1
|
||||
required = true
|
||||
end #add_arg_table
|
||||
|
||||
@add_arg_table! s["fix"] begin
|
||||
"--correct-ids"
|
||||
help = "Path to the list of correct ids"
|
||||
arg_type = AbstractPath
|
||||
range_tester = x -> exists(x) && isfile(x)
|
||||
required = true
|
||||
"--show", "-s"
|
||||
help = """
|
||||
Which types of fixes should be printed to output. Can be a
|
||||
combination of D for duplicate, F for fixed, N for not fixable,
|
||||
and V for valid.
|
||||
"""
|
||||
arg_type = ResultCodeOption
|
||||
default = ResultCodeOption("DFN")
|
||||
"--all", "-a"
|
||||
help = "Show all ids regardless of fixed status"
|
||||
action = :store_true
|
||||
"ids_to_be_corrected"
|
||||
help = "File containing CualerIDs that need correction"
|
||||
arg_type = AbstractPath
|
||||
range_tester = x -> exists(x) && isfile(x)
|
||||
required = true
|
||||
end #add_arg_table
|
||||
|
||||
@add_arg_table! s["convert"] begin
|
||||
"--length", "-l"
|
||||
help = "The length of the printed sample ids"
|
||||
arg_type = UInt64
|
||||
default = UInt64(5)
|
||||
"--fail-threshold", "-f"
|
||||
help = "NOT IMPLEMENTED: The rate at which similar ids will fail the process"
|
||||
arg_type = Float64
|
||||
range_tester = x -> x >= 0 && x <= 1.0
|
||||
default = 0.99
|
||||
"--to-cual-id"
|
||||
help = "Convert from CualerID to CualID"
|
||||
action = :store_true
|
||||
"ids_to_be_converted"
|
||||
help = "TSV file containing CualIDs or CualerIDs to be converted"
|
||||
arg_type = AbstractPath
|
||||
range_tester = x -> exists(x) && isfile(x)
|
||||
required = true
|
||||
end #@add_arg_table!
|
||||
|
||||
#! format: on
|
||||
|
||||
return s
|
||||
|
||||
end #function
|
||||
|
||||
const ARG_PARSE_TABLE = _create_arg_table()
|
||||
|
||||
function _create(parsed_args::Dict{Symbol,Any})
|
||||
if parsed_args[:_COMMAND_] == :ids
|
||||
_create_ids(parsed_args[:ids])
|
||||
elseif parsed_args[:_COMMAND_] == :labels
|
||||
_create_labels(parsed_args[:labels])
|
||||
end #if
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
function _create_ids(parsed_args::Dict{Symbol,Any})
|
||||
println("COMMAND:\tcreate ids")
|
||||
for (key, val) in parsed_args
|
||||
println("\t$key:\t$val")
|
||||
end #function
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
function _create_labels(parsed_args::Dict{Symbol,Any})
|
||||
println("COMMAND:\tcreate labels")
|
||||
for (key, val) in parsed_args
|
||||
println("\t$key:\t$val")
|
||||
end #function
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
function _fix(parsed_args::Dict{Symbol,Any})
|
||||
println("COMMAND:\tfix")
|
||||
for (key, val) in parsed_args
|
||||
println("\t$key:\t$val")
|
||||
end #function
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
function _convert(parsed_args::Dict{Symbol,Any})
|
||||
println("COMMAND:\tconvert")
|
||||
for (key, val) in parsed_args
|
||||
println("\t$key:\t$val")
|
||||
end #function
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
|
||||
function (@main)()
|
||||
parsed_args = parse_args(ARG_PARSE_TABLE; as_symbols = true)
|
||||
|
||||
if parsed_args[:_COMMAND_] == :create
|
||||
_create(parsed_args[:create])
|
||||
elseif parsed_args[:_COMMAND_] == :fix
|
||||
_fix(parsed_args[:fix])
|
||||
elseif parsed_args[:_COMMAND_] == :convert
|
||||
_convert(parsed_args[:convert])
|
||||
end #if
|
||||
|
||||
return 0
|
||||
end #function
|
||||
|
||||
end #module
|
||||
|
|
|
|||
120
src/ResultCodeOption.jl
Normal file
120
src/ResultCodeOption.jl
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""
|
||||
ResultCode::UInt8
|
||||
|
||||
Encodes a result of attempting to correct a CualerID
|
||||
|
||||
The valid codes are
|
||||
|
||||
- **D**: duplicate
|
||||
- **F**: fixed
|
||||
- **N**: not fixable
|
||||
- **V**: valid (no need for correction)
|
||||
"""
|
||||
@enum ResultCode::UInt8 begin
|
||||
D = UInt8(1)
|
||||
F = UInt8(2)
|
||||
N = UInt8(4)
|
||||
V = UInt8(8)
|
||||
end #enum
|
||||
|
||||
"""
|
||||
ResultCodeOption
|
||||
|
||||
Encodes potential corrected id codes.
|
||||
|
||||
The valid codes are
|
||||
|
||||
- **D**: duplicate
|
||||
- **F**: fixed
|
||||
- **N**: not fixable
|
||||
- **V**: valid (no need for correction)
|
||||
|
||||
The codes are implemented exactly as from cual-id and in that order
|
||||
|
||||
# Constructors
|
||||
|
||||
ResultCodeOption(str::AbstractString)
|
||||
|
||||
Create a `ResultCodeOption` based on a the character codes contained within
|
||||
`str`. `str` is case insensitive, may contain an arbitrary number of valid codes
|
||||
(as specified above), and may contain duplicates. A `ResultCodeOption` cannot be
|
||||
constructed from a combination of strings and bitmasks/integers.
|
||||
|
||||
# Extended help
|
||||
|
||||
The struct stores values as a bitmask
|
||||
|
||||
| Bit (Int) | Bit (UInt) | Description |
|
||||
| ---------:| ----------:| --------------- |
|
||||
| 1 | 0x01 | D - duplicate |
|
||||
| 2 | 0x02 | F - fixed |
|
||||
| 4 | 0x04 | N - not fixable |
|
||||
| 8 | 0x08 | V - valid |
|
||||
|
||||
"""
|
||||
struct ResultCodeOption
|
||||
val::UInt8
|
||||
end #struct
|
||||
|
||||
ResultCodeOption(str::AbstractString) = Base.parse(ResultCodeOption, str)
|
||||
|
||||
function Base.parse(::Type{ResultCodeOption}, str::AbstractString)
|
||||
upper_str = uppercase(str)
|
||||
|
||||
# Throw if there are any invalid codes
|
||||
occursin(r"[^DFNV]", upper_str) &&
|
||||
throw(DomainError("Valid `ResultCodeOption` must contain D, F, N, or V only"))
|
||||
|
||||
# Throw if there are no valid codes
|
||||
occursin(r"[DFNV]", upper_str) || throw(
|
||||
DomainError("Valid `ResultCodeOption` must contain at least one of D, F, N, or V"),
|
||||
)
|
||||
|
||||
result_code = sum(
|
||||
c -> occursin(string(Symbol(c)), upper_str) ? UInt8(c) : UInt8(0),
|
||||
instances(ResultCode),
|
||||
)
|
||||
|
||||
return ResultCodeOption(result_code)
|
||||
|
||||
end #function
|
||||
|
||||
function Base.show(io::IO, r::ResultCodeOption)
|
||||
print_code = ""
|
||||
|
||||
print(io, summary(r), " (")
|
||||
foreach(
|
||||
c -> r.val & UInt8(c) != 0 && print(io, string(Symbol(c))),
|
||||
instances(ResultCode),
|
||||
)
|
||||
print(io, ")")
|
||||
end #function
|
||||
|
||||
"""
|
||||
has_duplicate(r::ResultCodeOption)
|
||||
|
||||
Determines if `r` contains a duplicate result ([`ResultCode`](@ref) D)
|
||||
"""
|
||||
has_duplicate(r::ResultCodeOption) = r.val & UInt8(D) != 0
|
||||
|
||||
"""
|
||||
has_fixed(r::ResultCodeOption)
|
||||
|
||||
Determines if `r` contains a fixed result ([`ResultCode`](@ref) F)
|
||||
"""
|
||||
has_fixed(r::ResultCodeOption) = r.val & UInt8(F) != 0
|
||||
|
||||
"""
|
||||
has_not_fixable(r::ResultCodeOption)
|
||||
|
||||
Determines if `r` contains a not fixable result ([`ResultCode`](@ref) N)
|
||||
"""
|
||||
has_not_fixable(r::ResultCodeOption) = r.val & UInt8(N) != 0
|
||||
|
||||
"""
|
||||
has_valid(r::ResultCodeOption)
|
||||
|
||||
Determines if `r` contains a valid (not corrected) result
|
||||
([`ResultCode`](@ref) V)
|
||||
"""
|
||||
has_valid(r::ResultCodeOption) = r.val & UInt8(V) != 0
|
||||
5
test/Aqua.jl
Normal file
5
test/Aqua.jl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Aqua
|
||||
|
||||
@testset "Aqua" begin
|
||||
Aqua.test_all(CualerID)
|
||||
end #@testset
|
||||
21
test/Base30.jl
Normal file
21
test/Base30.jl
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using CualerID: base30encode, rebase
|
||||
|
||||
@testset "rebase" begin
|
||||
@test rebase(0, 10) == UInt8[1]
|
||||
@test rebase(0, 2) == UInt8[1]
|
||||
@test rebase(0, 32) == UInt8[1]
|
||||
|
||||
@test rebase(10, 10) == UInt8[2, 1]
|
||||
@test rebase(2, 2) == UInt8[2, 1]
|
||||
@test rebase(32, 32) == UInt8[2, 1]
|
||||
|
||||
@test rebase(11, 10) == UInt8[2, 2]
|
||||
@test rebase(3, 2) == UInt8[2, 2]
|
||||
@test rebase(33, 32) == UInt8[2, 2]
|
||||
end #@testset
|
||||
|
||||
@testset "base30encode" begin
|
||||
@test base30encode(0) == "0"
|
||||
@test base30encode(1) == "1"
|
||||
@test base30encode(30) == "10"
|
||||
end
|
||||
6
test/Doctests.jl
Normal file
6
test/Doctests.jl
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
using Documenter
|
||||
|
||||
@testset "doctests" begin
|
||||
DocMeta.setdocmeta!(CualerID, :DocTestSetup, :(using CualerID); recursive = true)
|
||||
doctest(CualerID)
|
||||
end
|
||||
|
|
@ -1,2 +1,10 @@
|
|||
[deps]
|
||||
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
|
||||
[compat]
|
||||
Aqua = "0.8"
|
||||
Documenter = "1.16"
|
||||
UUIDs = "1.11.0"
|
||||
|
|
|
|||
8
test/ResultCodeOption.jl
Normal file
8
test/ResultCodeOption.jl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@testset "ResultCode" begin
|
||||
@test_throws DomainError ResultCodeOption("A")
|
||||
@test_throws DomainError ResultCodeOption("")
|
||||
@test has_duplicate(ResultCodeOption("D"))
|
||||
@test has_fixed(ResultCodeOption("F"))
|
||||
@test has_not_fixable(ResultCodeOption("N"))
|
||||
@test has_valid(ResultCodeOption("V"))
|
||||
end #@testset
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using CualerID
|
||||
using Test
|
||||
|
||||
@testset "CualerID.jl" begin
|
||||
# Write your tests here.
|
||||
end
|
||||
include("Doctests.jl")
|
||||
include("Aqua.jl")
|
||||
include("Base30.jl")
|
||||
include("ResultCodeOption.jl")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue