diff --git a/README.md b/README.md index acefdfa..2238cde 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Simple encode & decode: ```julia config = Sqids.configure() -id = Sqids.encode(config, [1, 2, 3]) #> "8QRLaD" +id = Sqids.encode(config, [1, 2, 3]) #> "86Rf07" numbers = Sqids.decode(config, id) #> [1, 2, 3] ``` @@ -34,7 +34,7 @@ Randomize IDs by providing a custom alphabet: ```julia config = Sqids.configure(alphabet="FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE") -id = Sqids.encode(config, [1, 2, 3]) #> "B5aMa3" +id = Sqids.encode(config, [1, 2, 3]) #> "B4aajs" numbers = Sqids.decode(config, id) #> [1, 2, 3] ``` @@ -42,7 +42,7 @@ Enforce a *minimum* length for IDs: ```julia config = Sqids.configure(minLength=10) -id = Sqids.encode(config, [1, 2, 3]) #> "75JT1cd0dL" +id = Sqids.encode(config, [1, 2, 3]) #> "86Rf07xd4z" numbers = Sqids.decode(config, id) #> [1, 2, 3] ``` @@ -50,7 +50,7 @@ Prevent specific words from appearing anywhere in the auto-generated IDs: ```julia config = Sqids.configure(blocklist=["word1","word2"]) -id = Sqids.encode(config, [1, 2, 3]) #> "8QRLaD" +id = Sqids.encode(config, [1, 2, 3]) #> "86Rf07" numbers = Sqids.decode(config, id) #> [1, 2, 3] ``` @@ -60,7 +60,7 @@ If `strict=false` is set when configuring, it enables handling of limitless valu ```julia config = Sqids.configure(strict=false) # not-strict mode -id = Sqids.encode(config, Int128[9223372036854775808]) #> "piF3yT7tOtoO" +id = Sqids.encode(config, Int128[9223372036854775808]) #> "pXFNc5r689z6" numbers = Sqids.decode(config, id) #> Int128[9223372036854775808] ``` diff --git a/src/Sqids.jl b/src/Sqids.jl index 5b9a654..fbc3b2e 100644 --- a/src/Sqids.jl +++ b/src/Sqids.jl @@ -1,6 +1,6 @@ module Sqids -export encode, decode, minValue, maxValue +export encode, decode using Base.Checked: mul_with_overflow, add_with_overflow @@ -8,6 +8,7 @@ include("Blocklists.jl") const DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const MIN_VALUE = 0 +const MIN_LENGTH_LIMIT = 1_000 _shuffle(alphabet::AbstractString) = String(_shuffle!(collect(alphabet))) function _shuffle!(chars::Vector{Char}) @@ -24,7 +25,7 @@ end Sqids.Configuration Sqids' parameter-configuration. -Be sure to place the instance as the 1st argument of [`encode`](@ref), [`decode`](@ref), [`minValue`](@ref) (and [`maxValue`](@ref)). +Be sure to place the instance as the 1st argument of [`encode`](@ref) and [`decode`](@ref). See also: [`configure`](@ref) """ @@ -34,14 +35,15 @@ struct Configuration{S} blocklist::Set{String} function Configuration(alphabet::AbstractString, minLength::Int, blocklist, strict::Bool = true) # @assert blocklist isa Union{AbstractSet{<:AbstractString}, AbstractArray{<:AbstractString}} - length(alphabet) < 5 && throw(ArgumentError("Alphabet length must be at least 5.")) + sizeof(alphabet) == length(alphabet) || throw(ArgumentError("Alphabet cannot contain multibyte characters.")) + length(alphabet) < 3 && throw(ArgumentError("Alphabet length must be at least 3.")) length(unique(alphabet)) == length(alphabet) || throw(ArgumentError("Alphabet must contain unique characters.")) - MIN_VALUE ≤ minLength ≤ length(alphabet) || throw(ArgumentError("Minimum length has to be between $(MIN_VALUE) and $(length(alphabet)).")) + 0 ≤ minLength ≤ MIN_LENGTH_LIMIT || throw(ArgumentError("Minimum length has to be between 0 and $(MIN_LENGTH_LIMIT).")) - # clean up blocklist: - # 1. all blocklist words should be lowercase - # 2. no words less than 3 chars - # 3. if some words contain chars that are not in the alphabet, remove those + # clean up blocklist: + # 1. all blocklist words should be lowercase + # 2. no words less than 3 chars + # 3. if some words contain chars that are not in the alphabet, remove those alphabet_chars = Set(lowercase(alphabet)) filteredBlocklist = Set(filter(blocklist .|> lowercase) do word length(word) ≥ 3 && issetequal(word ∩ alphabet_chars, word) @@ -93,53 +95,55 @@ Encode the passed `numbers` to an id. # Example ```julia-repl julia> encode(Sqids.configure(), [1, 2, 3]) -"8QRLaD" +"86Rf07" ``` """ function encode(config::Configuration, numbers::AbstractArray{<:Integer}) isempty(numbers) && return "" # don't allow out-of-range numbers [might be lang-specific] - all(≥(minValue(config)), numbers) || throw(ArgumentError("Encoding supports numbers greater than or equal to $(minValue(config))")) - _encode_numbers(config, numbers, false) + all(≥(MIN_VALUE), numbers) || throw(ArgumentError("Encoding supports numbers greater than or equal to $(MIN_VALUE).")) + _encode_numbers(config, numbers, 0) end function encode(config::Configuration{true}, numbers::AbstractArray{<:Integer}) isempty(numbers) && return "" # don't allow out-of-range numbers [might be lang-specific] all(numbers) do num - minValue(config) ≤ num ≤ maxValue(config) - end || throw(ArgumentError("Encoding supports numbers between $(minValue(config)) and $(maxValue(config))")) - _encode_numbers(config, numbers, false) + MIN_VALUE ≤ num ≤ maxValue(config) + end || throw(ArgumentError("Encoding supports numbers between $(MIN_VALUE) and $(maxValue(config)).")) + _encode_numbers(config, numbers, 0) end -function _encode_numbers(config::Configuration, numbers::AbstractArray{<:Integer}, partitioned::Bool = false) +function _encode_numbers(config::Configuration, numbers::AbstractArray{<:Integer}, increment::Int = 0) + # if increment is greater than alphabet length, we've reached max attempts + if increment > length(config.alphabet) + throw(ArgumentError("Reached max attempts to re-generate the ID.")) + end + # get a semi-random offset from input numbers - # offset = foldl((a, (i, v)) -> a + Int(config.alphabet[v % length(config.alphabet) + 1]) + i, enumerate(numbers), init=0) % length(config.alphabet) + # offset = foldl((a, (i, v)) -> a + Int(config.alphabet[v % length(config.alphabet) + 1]) + i, enumerate(numbers), init=increment) % length(config.alphabet) # ↓ a little faster - offset = 0 + offset = increment for (i, v) in pairs(numbers) offset += Int(config.alphabet[v % length(config.alphabet) + 1]) + i end offset %= length(config.alphabet) # prefix is the first character in the generated ID, used for randomization - # partition is the character used instead of the first separator to indicate that the first number in the input array is a throwaway number. this character is used only once to handle blocklist and/or padding. it's omitted completely in all other cases - # alphabet should not contain `prefix` or `partition` reserved characters + # reverse alphabet (otherwise for [0, x] `offset` and `separator` will be the same char) alphabet_chars = collect(config.alphabet)[[offset+1:end; begin:offset]] - prefix = popfirst!(alphabet_chars) - partition = popfirst!(alphabet_chars) + prefix = alphabet_chars[begin] + reverse!(alphabet_chars) id = sprint(sizehint=2*length(numbers)) do io print(io, prefix) # encode input array for (i, num) in pairs(numbers) - # the last character of the alphabet is going to be reserved for the `separator` - alphabetWithoutSeparator = @view alphabet_chars[begin:end-1] + # the first character of the alphabet is going to be reserved for the `separator` + alphabetWithoutSeparator = @view alphabet_chars[begin+1:end] print(io, _to_id(num, alphabetWithoutSeparator)) if i < length(numbers) - # prefix is used only for the first number - # separator = alphabet[end] - # for the barrier use the `separator` unless this is the first iteration and the first number is a throwaway number - then use the `partition` character - print(io, partitioned && i == 1 ? partition : alphabet_chars[end]) + # `separator` character is used to isolate numbers within the ID + print(io, alphabet_chars[begin]) # shuffle on every iteration _shuffle!(alphabet_chars) @@ -147,34 +151,22 @@ function _encode_numbers(config::Configuration, numbers::AbstractArray{<:Integer end end - # if `minLength` is used and the ID is too short, add a throwaway number + # handle `minLength` requirement, if the ID is too short if config.minLength > length(id) - # partitioning is required so we can safely throw away chunk of the ID during decoding - if !partitioned - partitioned_numbers = [zero(eltype(numbers)); numbers] - id = _encode_numbers(config, partitioned_numbers, true) - end - - # if adding a `partition` number did not make the length meet the `minLength` requirement, then make the new id this format: `prefix` character + a slice of the alphabet to make up the missing length + the rest of the ID without the `prefix` character - if config.minLength > length(id) - id = id[begin] * join(alphabet_chars[begin:config.minLength - length(id)]) * id[2:end] + # append a separator + id *= alphabet_chars[begin] + + # keep appending `separator` + however much alphabet is needed + # for decoding: two separators next to each other is what tells us the rest are junk characters + while length(id) < config.minLength + _shuffle!(alphabet_chars) + id *= join(alphabet_chars[begin:min(config.minLength - length(id), length(alphabet_chars))]) end end - # if ID has a blocked word anywhere, add a throwaway number & start over + # if ID has a blocked word anywhere, restart with a +1 increment if _is_blocked_id(config, id) - if partitioned - # c8 ignore next 2 - if isstrict(config) && numbers[1] == maxValue(config) - throw(ArgumentError("Ran out of range checking against the blocklist")) - else - numbers[1] += 1 - id = _encode_numbers(config, numbers, true) - end - else - partitioned_numbers = [zero(eltype(numbers)); numbers] - id = _encode_numbers(config, partitioned_numbers, true) - end + id = _encode_numbers(config, numbers, increment + 1) end return id @@ -220,7 +212,7 @@ Restore a numbers list from the passed `id`. # Example ```julia-repl -julia> decode(Sqids.configure(), "8QRLaD") +julia> decode(Sqids.configure(), "86Rf07") 3-element Array{Int64,1}: 1 2 @@ -246,29 +238,23 @@ function decode(config::Configuration, id::AbstractString) offset = findfirst(==(prefix), config.alphabet) # re-arrange alphabet back into it's original form - # `partition` character is in second position - # alphabet has to be without reserved `prefix` & `partition` characters - alphabet_chars = collect(config.alphabet)[[offset+1:end; begin:offset-1]] - partition = popfirst!(alphabet_chars) + # reverse alphabet + alphabet_chars = collect(config.alphabet)[[offset:end; begin:offset-1]] + reverse!(alphabet_chars) # now it's safe to remove the prefix character from ID, it's not needed anymore id_wk = @view id[begin+1:end] - # if this ID contains the `partition` character (between 1st position and non-last position), throw away everything to the left of it, include the `partition` character - partition_index = findfirst(==(partition), id_wk) - if !isnothing(partition_index) && partition_index > 1 && partition_index < length(id_wk) - id_wk = @view id_wk[partition_index+1:end] - alphabet_chars = _shuffle!(alphabet_chars) - end - # decode while !isempty(id_wk) - separator = alphabet_chars[end] + separator = alphabet_chars[begin] chunks = split(id_wk, separator, limit=2) + # if chunk is empty, we are done (the rest are junk characters) + isempty(chunks[1]) && return ret # decode the number without using the `separator` character - # but also check that ID can be decoded (eg: does not contain any non-alphabet characters) - alphabetWithoutSeparator = @view alphabet_chars[begin:end-1] - chunks[1] ⊆ alphabetWithoutSeparator || return Int[] + # # but also check that ID can be decoded (eg: does not contain any non-alphabet characters) + alphabetWithoutSeparator = @view alphabet_chars[begin+1:end] + # chunks[1] ⊆ alphabetWithoutSeparator || return Int[] # push!(ret, _to_number(config, chunks[1], alphabetWithoutSeparator)) num = _to_number(config, chunks[1], alphabetWithoutSeparator) if !isstrict(config) @@ -317,23 +303,11 @@ function _to_number(config::Configuration, id::AbstractString, init::I, alphabet result end -""" - minValue(config::Sqids.Configuration) - -Return the minimum value available with Sqids. -Always returns `0`. - -See also: [`maxValue`](@ref) -""" -minValue(::Configuration) = MIN_VALUE - """ maxValue(config::Sqids.Configuration) Return the maximum value available with Sqids. Returns `typemax(Int)` if Strict mode, or throws an `MethodError` otherwise. - -See also: [`minValue`](@ref) """ maxValue(::Configuration{true}) = typemax(Int) diff --git a/test/alphabet.jl b/test/alphabet.jl index 8ffd79c..01cc624 100644 --- a/test/alphabet.jl +++ b/test/alphabet.jl @@ -9,14 +9,14 @@ using Test config = Sqids.configure(alphabet="0123456789abcdef") numbers = [1, 2, 3] - id = "4d9fd2" + id = "489158" @test Sqids.encode(config, numbers) == id @test Sqids.decode(config, id) == numbers end @testset "short alphabet" begin - config = Sqids.configure(alphabet="abcde") + config = Sqids.configure(alphabet="abc") numbers = [1, 2, 3] @test Sqids.decode(config, Sqids.encode(config, numbers)) == numbers @@ -29,15 +29,36 @@ using Test @test Sqids.decode(config, Sqids.encode(config, numbers)) == numbers end + @testset "multibyte characters" begin + @test_throws ArgumentError begin + Sqids.configure(alphabet="ë1092") + end + @static if VERSION ≥ v"1.8.0" + @test_throws "Alphabet cannot contain multibyte characters" begin + Sqids.configure(alphabet="ë1092") + end + end + end + @testset "repeating alphabet characters" begin @test_throws ArgumentError begin Sqids.configure(alphabet="aabcdefg") end + @static if VERSION ≥ v"1.8.0" + @test_throws "Alphabet must contain unique characters" begin + Sqids.configure(alphabet="aabcdefg") + end + end end @testset "too short of an alphabet" begin @test_throws ArgumentError begin - Sqids.configure(alphabet="abcd") + Sqids.configure(alphabet="ab") + end + @static if VERSION ≥ v"1.8.0" + @test_throws "Alphabet length must be at least 3" begin + Sqids.configure(alphabet="ab") + end end end diff --git a/test/blocklist.jl b/test/blocklist.jl index 1c4c09e..7a0fe71 100644 --- a/test/blocklist.jl +++ b/test/blocklist.jl @@ -7,61 +7,72 @@ using Test @testset "if no custom blocklist param, use the default blocklist" begin config = Sqids.configure() - @test Sqids.decode(config, "sexy") == [200044] - @test Sqids.encode(config, [200044]) == "d171vI" + @test Sqids.decode(config, "aho1e") == [4572721] + @test Sqids.encode(config, [4572721]) == "JExTR" end @testset "if an empty blocklist param passed, don't use any blocklist" begin config = Sqids.configure(blocklist=[]) - @test Sqids.decode(config, "sexy") == [200044] - @test Sqids.encode(config, [200044]) == "sexy" + @test Sqids.decode(config, "aho1e") == [4572721] + @test Sqids.encode(config, [4572721]) == "aho1e" end @testset "if a non-empty blocklist param passed, use only that" begin - config = Sqids.configure(blocklist=["AvTg"]) - @test Sqids.decode(config, "sexy") == [200044] - @test Sqids.encode(config, [200044]) == "sexy" - @test Sqids.decode(config, "AvTg") == [100000] - @test Sqids.encode(config, [100000]) == "7T1X8k" - @test Sqids.decode(config, "7T1X8k") == [100000] + config = Sqids.configure(blocklist=["ArUO"]) + @test Sqids.decode(config, "aho1e") == [4572721] + @test Sqids.encode(config, [4572721]) == "aho1e" + @test Sqids.decode(config, "ArUO") == [100000] + @test Sqids.encode(config, [100000]) == "QyG4" + @test Sqids.decode(config, "QyG4") == [100000] end @testset "blocklist" begin config = Sqids.configure(blocklist=[ - "8QRLaD", # normal result of 1st encoding, let's block that word on purpose - "7T1cd0dL", # result of 2nd encoding - "UeIe", # result of 3rd encoding is `RA8UeIe7`, let's block a substring - "imhw", # result of 4th encoding is `WM3Limhw`, let's block the postfix - "LfUQ", # result of 4th encoding is `LfUQh4HN`, let's block the prefix + "JSwXFaosAN", # normal result of 1st encoding, let's block that word on purpose + "OCjV9JK64o", # result of 2nd encoding + "rBHf", # result of 3rd encoding is `4rBHfOiqd3`, let's block a substring + "79SM", # result of 4th encoding is `dyhgw479SM`, let's block the postfix + "7tE6", # result of 4th encoding is `7tE6jdAHLe`, let's block the prefix ]) - @test Sqids.encode(config, [1, 2, 3]) == "TM0x1Mxz" - @test Sqids.decode(config, "TM0x1Mxz") == [1, 2, 3] + @test Sqids.encode(config, [1_000_000, 2_000_000]) == "1aYeB7bRUt" + @test Sqids.decode(config, "1aYeB7bRUt") == [1_000_000, 2_000_000] end @testset "decoding blocked words should still work" begin - config = Sqids.configure(blocklist=["8QRLaD", "7T1cd0dL", "RA8UeIe7", "WM3Limhw", "LfUQh4HN"]) - @test Sqids.decode(config, "8QRLaD") == [1, 2, 3] - @test Sqids.decode(config, "7T1cd0dL") == [1, 2, 3] - @test Sqids.decode(config, "RA8UeIe7") == [1, 2, 3] - @test Sqids.decode(config, "WM3Limhw") == [1, 2, 3] - @test Sqids.decode(config, "LfUQh4HN") == [1, 2, 3] + config = Sqids.configure(blocklist=["86Rf07", "se8ojk", "ARsz1p", "Q8AI49", "5sQRZO"]) + @test Sqids.decode(config, "86Rf07") == [1, 2, 3] + @test Sqids.decode(config, "se8ojk") == [1, 2, 3] + @test Sqids.decode(config, "ARsz1p") == [1, 2, 3] + @test Sqids.decode(config, "Q8AI49") == [1, 2, 3] + @test Sqids.decode(config, "5sQRZO") == [1, 2, 3] end @testset "match against a short blocked word" begin - config = Sqids.configure(blocklist=["pPQ"]) + config = Sqids.configure(blocklist=["pnd"]) @test Sqids.decode(config, Sqids.encode(config, [1000])) == [1000] end @testset "blocklist filtering in constructor" begin - config = Sqids.configure(alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ", blocklist=["sqnmpn"]) + config = Sqids.configure(alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ", blocklist=["sxnzkl"]) id = Sqids.encode(config, [1, 2, 3]) numbers = Sqids.decode(config, id) - @test id == "ULPBZGBM" # without blocklist, would've been "SQNMPN" + @test id == "IBSHOZ" # without blocklist, would've been "SXNZKL" @test numbers == [1, 2, 3] end + @testset "max encoding attempts" begin + config = Sqids.configure(alphabet="abc", minLength=3, blocklist=["cab", "abc", "bca"]) + @test_throws ArgumentError begin + Sqids.encode(config, [0]) + end + @static if VERSION ≥ v"1.8.0" + @test_throws "Reached max attempts to re-generate the ID" begin + Sqids.encode(config, [0]) + end + end + end end end # module BlocklistTests \ No newline at end of file diff --git a/test/encoding.jl b/test/encoding.jl index 19b1a43..975c24e 100644 --- a/test/encoding.jl +++ b/test/encoding.jl @@ -9,7 +9,7 @@ using Test config = Sqids.configure() numbers = [1, 2, 3] - id = "8QRLaD" + id = "86Rf07" @test Sqids.encode(config, numbers) == id @test Sqids.decode(config, id) == numbers @@ -18,7 +18,6 @@ using Test @testset "different inputs" begin config = Sqids.configure() - # numbers = [0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, Sqids.maxValue()] numbers = [0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, typemax(Int)] @test Sqids.decode(config, Sqids.encode(config, numbers)) == numbers end @@ -27,16 +26,16 @@ using Test config = Sqids.configure() ids = Dict( - "bV" => [0], - "U9" => [1], - "g8" => [2], - "Ez" => [3], - "V8" => [4], - "ul" => [5], - "O3" => [6], - "AF" => [7], - "ph" => [8], - "n8" => [9] + "bM" => [0], + "Uk" => [1], + "gb" => [2], + "Ef" => [3], + "Vq" => [4], + "uw" => [5], + "OI" => [6], + "AX" => [7], + "p6" => [8], + "nJ" => [9], ) for (id, numbers) in ids @@ -49,16 +48,16 @@ using Test config = Sqids.configure() ids = Dict( - "SrIu" => [0, 0], - "nZqE" => [0, 1], - "tJyf" => [0, 2], - "e86S" => [0, 3], - "rtC7" => [0, 4], - "sQ8R" => [0, 5], - "uz2n" => [0, 6], - "7Td9" => [0, 7], - "3nWE" => [0, 8], - "mIxM" => [0, 9] + "SvIz" => [0, 0], + "n3qa" => [0, 1], + "tryF" => [0, 2], + "eg6q" => [0, 3], + "rSCF" => [0, 4], + "sR8x" => [0, 5], + "uY2M" => [0, 6], + "74dI" => [0, 7], + "30WX" => [0, 8], + "moxr" => [0, 9], ) for (id, numbers) in ids @@ -71,16 +70,16 @@ using Test config = Sqids.configure() ids = Dict( - "SrIu" => [0, 0], - "nbqh" => [1, 0], - "t4yj" => [2, 0], - "eQ6L" => [3, 0], - "r4Cc" => [4, 0], - "sL82" => [5, 0], - "uo2f" => [6, 0], - "7Zdq" => [7, 0], - "36Wf" => [8, 0], - "m4xT" => [9, 0] + "SvIz" => [0, 0], + "nWqP" => [1, 0], + "tSyw" => [2, 0], + "eX68" => [3, 0], + "rxCY" => [4, 0], + "sV8a" => [5, 0], + "uf2K" => [6, 0], + "7Cdk" => [7, 0], + "3aWP" => [8, 0], + "m2xn" => [9, 0], ) for (id, numbers) in ids @@ -123,26 +122,25 @@ using Test @test Sqids.decode(config, "*") == Int[] end - @testset "decoding an invalid ID with a repeating reserved character" begin - config = Sqids.configure() - - @test Sqids.decode(config, "fff") == Int[] - end - @testset "encode out-of-range numbers" begin config = Sqids.configure() - @test_throws ArgumentError Sqids.encode(config, [Sqids.minValue(config) - 1]) - # @test_throws ArgumentError Sqids.encode(config, [Sqids.maxValue(config) + 1]) - @test_throws ArgumentError Sqids.encode(config, [big(Sqids.maxValue(config)) + 1]) + @test_throws ArgumentError Sqids.encode(config, [-1]) + # @test_throws ArgumentError Sqids.encode(config, [typemax(Int) + 1]) + @test_throws ArgumentError Sqids.encode(config, [widen(typemax(Int)) + 1]) + @static if VERSION ≥ v"1.8.0" + @test_throws r"Encoding supports numbers between 0 and \d+" Sqids.encode(config, [-1]) + @test_throws "Encoding supports numbers between 0 and $(typemax(Int))" Sqids.encode(config, [widen(typemax(Int)) + 1]) + end end - @testset "decode to out-of-range numbers" begin - config = Sqids.configure() + # TODO: Check to activate or not to activate this test + # @testset "decode to out-of-range numbers" begin + # config = Sqids.configure() - @test_throws ArgumentError Sqids.decode(config, "piF3yT7tOtoO") # decoded to Int128[9223372036854775808] if not-strict mode - @test_throws ArgumentError Sqids.decode(config, "Vpe9SEjlSQreM3A2DNrRLZt") # decoded to BigInt[170141183460469231731687303715884105728] if not-strict mode - end + # @test_throws ArgumentError Sqids.decode(config, "piF3yT7tOtoO") # decoded to Int128[9223372036854775808] if not-strict mode + # @test_throws ArgumentError Sqids.decode(config, "Vpe9SEjlSQreM3A2DNrRLZt") # decoded to BigInt[170141183460469231731687303715884105728] if not-strict mode + # end end end # module EncodingTests \ No newline at end of file diff --git a/test/minlength.jl b/test/minlength.jl index 57fdba1..3bdc7a5 100644 --- a/test/minlength.jl +++ b/test/minlength.jl @@ -9,26 +9,56 @@ using Test config = Sqids.configure(minLength=length(Sqids.DEFAULT_ALPHABET)) numbers = [1, 2, 3] - id = "75JILToVsGerOADWmHlY38xvbaNZKQ9wdFS0B6kcMEtnRpgizhjU42qT1cd0dL" + id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM" @test Sqids.encode(config, numbers) == id @test Sqids.decode(config, id) == numbers end + @testset "incremental" begin + numbers = [1, 2, 3] + length_map = Dict( + 6 => "86Rf07", + 7 => "86Rf07x", + 8 => "86Rf07xd", + 9 => "86Rf07xd4", + 10 => "86Rf07xd4z", + 11 => "86Rf07xd4zB", + 12 => "86Rf07xd4zBm", + 13 => "86Rf07xd4zBmi", + length(Sqids.DEFAULT_ALPHABET) + 0 => + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM", + length(Sqids.DEFAULT_ALPHABET) + 1 => + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy", + length(Sqids.DEFAULT_ALPHABET) + 2 => + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf", + length(Sqids.DEFAULT_ALPHABET) + 3 => + "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1", + ) + + for (minLength, id) in length_map + config = Sqids.configure(minLength=minLength) + + @test Sqids.encode(config, numbers) == id + @test length(Sqids.encode(config, numbers)) == minLength + @test Sqids.decode(config, id) == numbers + end + end + @testset "incremental numbers" begin config = Sqids.configure(minLength=length(Sqids.DEFAULT_ALPHABET)) ids = Dict( - "jf26PLNeO5WbJDUV7FmMtlGXps3CoqkHnZ8cYd19yIiTAQuvKSExzhrRghBlwf" => [0, 0], - "vQLUq7zWXC6k9cNOtgJ2ZK8rbxuipBFAS10yTdYeRa3ojHwGnmMV4PDhESI2jL" => [0, 1], - "YhcpVK3COXbifmnZoLuxWgBQwtjsSaDGAdr0ReTHM16yI9vU8JNzlFq5Eu2oPp" => [0, 2], - "OTkn9daFgDZX6LbmfxI83RSKetJu0APihlsrYoz5pvQw7GyWHEUcN2jBqd4kJ9" => [0, 3], - "h2cV5eLNYj1x4ToZpfM90UlgHBOKikQFvnW36AC8zrmuJ7XdRytIGPawqYEbBe" => [0, 4], - "7Mf0HeUNkpsZOTvmcj836P9EWKaACBubInFJtwXR2DSzgYGhQV5i4lLxoT1qdU" => [0, 5], - "APVSD1ZIY4WGBK75xktMfTev8qsCJw6oyH2j3OnLcXRlhziUmpbuNEar05QCsI" => [0, 6], - "P0LUhnlT76rsWSofOeyRGQZv1cC5qu3dtaJYNEXwk8Vpx92bKiHIz4MgmiDOF7" => [0, 7], - "xAhypZMXYIGCL4uW0te6lsFHaPc3SiD1TBgw5O7bvodzjqUn89JQRfk2Nvm4JI" => [0, 8], - "94dRPIZ6irlXWvTbKywFuAhBoECQOVMjDJp53s2xeqaSzHY8nc17tmkLGwfGNl" => [0, 9], + "SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu" => [0, 0], + "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc" => [0, 1], + "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ" => [0, 2], + "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE" => [0, 3], + "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX" => [0, 4], + "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2" => [0, 5], + "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0" => [0, 6], + "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy" => [0, 7], + "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS" => [0, 8], + "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin" => [0, 9], ) for (id, numbers) in ids @@ -38,16 +68,14 @@ using Test end @testset "min lengths" begin - _config = Sqids.configure() for minLength in [0, 1, 5, 10, length(Sqids.DEFAULT_ALPHABET)] for numbers in [ - [Sqids.minValue(_config)], + [0], [0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [100, 200, 300], [1_000, 2_000, 3_000], [1_000_000], - # [Sqids.maxValue()], [typemax(Int)], ] config = Sqids.configure(minLength=minLength) @@ -61,7 +89,11 @@ using Test @testset "out-of-range invalid min length" begin @test_throws ArgumentError Sqids.configure(minLength=-1) - @test_throws ArgumentError Sqids.configure(minLength=length(Sqids.DEFAULT_ALPHABET) + 1) + @test_throws ArgumentError Sqids.configure(minLength=1_000 + 1) + @static if VERSION ≥ v"1.8.0" + @test_throws r"Minimum length has to be between 0 and \d+" Sqids.configure(minLength=-1) + @test_throws "Minimum length has to be between 0 and $(Sqids.MIN_LENGTH_LIMIT)" Sqids.configure(minLength=Sqids.MIN_LENGTH_LIMIT + 1) + end end end diff --git a/test/notstrict.jl b/test/notstrict.jl index 81e8159..71bd311 100644 --- a/test/notstrict.jl +++ b/test/notstrict.jl @@ -23,7 +23,7 @@ using Test config = Sqids.configure(strict=false) numbers = [1, 2, 3] - id = "8QRLaD" + id = "86Rf07" @test Sqids.encode(config, numbers) == id @test Sqids.decode(config, id) == numbers @@ -32,12 +32,12 @@ using Test @testset "encode/decode out-of-range numbers" begin config = Sqids.configure(strict=false) - @test_throws ArgumentError Sqids.encode(config, [Sqids.minValue(config) - 1]) + @test_throws ArgumentError Sqids.encode(config, [-1]) @test_throws MethodError Sqids.maxValue(config) - @test Sqids.encode(config, [widen(typemax(Int64)) + 1]) == "piF3yT7tOtoO" - @test Sqids.decode(config, "piF3yT7tOtoO") == [widen(typemax(Int64)) + 1] - @test Sqids.encode(config, [big(typemax(Int128)) + 1]) == "Vpe9SEjlSQreM3A2DNrRLZt" - @test Sqids.decode(config, "Vpe9SEjlSQreM3A2DNrRLZt") == [big(typemax(Int128)) + 1] + @test Sqids.encode(config, [widen(typemax(Int64)) + 1]) == "pXFNc5r689z6" + @test Sqids.decode(config, "pXFNc5r689z6") == [widen(typemax(Int64)) + 1] + @test Sqids.encode(config, [big(typemax(Int128)) + 1]) == "V3PI8qa3oBSPJ0E8hID5F0W" + @test Sqids.decode(config, "V3PI8qa3oBSPJ0E8hID5F0W") == [big(typemax(Int128)) + 1] end @testset "different inputs" begin diff --git a/test/shuffle.jl b/test/shuffle.jl index 9cb3965..1e90261 100644 --- a/test/shuffle.jl +++ b/test/shuffle.jl @@ -63,14 +63,6 @@ using Test @test Sqids._shuffle(i) == o end - @testset "bars" begin - @test Sqids._shuffle("▁▂▃▄▅▆▇█") == "▂▇▄▅▆▃▁█" - end - - @testset "bars with numbers" begin - @test Sqids._shuffle("▁▂▃▄▅▆▇█0123456789") == "14▅▂▇320▆75▄█96▃8▁" - end - end end # module ShuffleTests \ No newline at end of file