From 3328b36a36d2e1ba3a334ef0c3315a8e8f9cfa1c Mon Sep 17 00:00:00 2001 From: Heptazhou Date: Sun, 3 Apr 2022 16:09:45 +0000 Subject: [PATCH] Create --- .github/workflows/CI.yml | 45 +++++++++ .gitignore | 3 + Project.toml | 14 +++ README.md | 15 +++ src/UUID4.jl | 204 +++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 79 +++++++++++++++ 6 files changed, 360 insertions(+) create mode 100644 .github/workflows/CI.yml create mode 100644 .gitignore create mode 100644 Project.toml create mode 100644 README.md create mode 100644 src/UUID4.jl create mode 100644 test/runtests.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..5f79393 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,45 @@ +name: CI +on: + pull_request: + branches: + - "master" + push: + branches: + - "master" + tags: + - "*" +defaults: + run: + shell: bash +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - windows-latest + - ubuntu-latest + - macos-latest + julia-version: + - "1.6" + - "1" + - "nightly" + exclude: + - os: macos-latest + julia-version: nightly + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + - uses: julia-actions/setup-julia@v1 + with: + show-versioninfo: true + version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: heptazhou/julia-codecov@v1 + - uses: codecov/codecov-action@v3 + with: + file: lcov.info diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..214f83f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +lcov.info +Manifest.toml diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..912a55f --- /dev/null +++ b/Project.toml @@ -0,0 +1,14 @@ +name = "UUID4" +uuid = "379725f3-1ad5-416d-b88a-50ced391fe04" +authors = ["Heptazhou "] +version = "1.8.0" + +[deps] +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..cda514a --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# UUID4.jl +[![CI status](https://github.com/0h7z/uuid4.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/0h7z/uuid4.jl/actions/workflows/CI.yml) +[![codecov.io](https://codecov.io/gh/0h7z/uuid4.jl/branch/master/graph/badge.svg)](https://app.codecov.io/gh/0h7z/uuid4.jl) + +***** +## Usage +```julia +pkg> registry add https://github.com/0h7z/0hjl.git +pkg> add UUID4 + +julia> using UUID4 +help?> UUID4 +help?> uuid +``` + diff --git a/src/UUID4.jl b/src/UUID4.jl new file mode 100644 index 0000000..1da92b0 --- /dev/null +++ b/src/UUID4.jl @@ -0,0 +1,204 @@ +# Copyright (C) 2022-2023 Heptazhou +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" + UUID4 + +The `UUID4` module provides universally unique identifier (UUID), version 4, +along with related functions. +""" +module UUID4 + +export uuid, uuid4 +export uuid_formats +export uuid_parse +export uuid_string +export uuid_version + +export AbstractRNG, MersenneTwister, RandomDevice +export LittleDict, OrderedDict +export UUID +const UUID = Base.UUID +using OrderedCollections: LittleDict, OrderedDict +using Random: AbstractRNG, MersenneTwister, RandomDevice + +""" + uuid(rng::AbstractRNG = RandomDevice()) -> UUID + +Generate a version 4 (random or pseudo-random) universally unique identifier +(UUID), as specified by [RFC 4122](https://www.ietf.org/rfc/rfc4122). + +# Examples +```jldoctest +julia> rng = MersenneTwister(42); + +julia> uuid(rng) +UUID("bc8f8f98-a497-45c4-817b-b034d1a24a0e") +``` +""" +function uuid(rng::AbstractRNG = RandomDevice())::UUID + id = rand(rng, UInt128) + id &= 0xffffffffffff0fff3fffffffffffffff + id |= 0x00000000000040008000000000000000 + id |> UUID +end +const uuid4 = uuid + +""" + uuid_formats() -> Vector{Int} + +Return all supported UUID string formats. +""" +function uuid_formats()::Vector{Int} + [ + # case-sensitive + 22 # (base62) 22 + 24 # (base62) 7-7-8 + # case-insensitive + 25 # (base36) 25 + 29 # (base36) 5-5-5-5-5 + 32 # (base16) 32 + 36 # (base16) 8-4-4-4-12 + 39 # (base16) 4-4-4-4-4-4-4-4 + ] +end + +""" + uuid_parse(str::String; fmt::Int = length(str)) -> Tuple{Int, UUID} +""" +function uuid_parse end +function uuid_parse(str::UUID; fmt::Any = 0x0)::Tuple{Int, UUID} + uuid_parse(string(str); fmt = Int(fmt)) +end +function uuid_parse(str::Any; fmt::Number = 0)::Tuple{Int, UUID} + uuid_parse(String(str); fmt = Int(fmt)) +end +function uuid_parse(str::String; fmt::Int = 0)::Tuple{Int, UUID} + len = length(str) + ret = if 0 > fmt + argumenterror("Invalid format `$fmt` (should be positive)") + elseif len ≠ fmt > 0 + argumenterror("Invalid id `$str` with length = $len (should be $fmt)") + elseif len ≡ 24 + uuid_parse(replace((str), "-" => ""), fmt = 22)[end] + elseif len ≡ 29 + uuid_parse(replace((str), "-" => ""), fmt = 25)[end] + elseif len ≡ 39 + uuid_parse(replace((str), "-" => ""), fmt = 32)[end] + elseif len ≡ 22 + UUID(parse(UInt128, str, base = 62)) + elseif len ≡ 25 + UUID(parse(UInt128, str, base = 36)) + elseif len ≡ 32 + UUID(parse(UInt128, str, base = 16)) + elseif len ≡ 36 + UUID(str) + else + argumenterror("Invalid id `$str` with length = $len") + end + len, ret +end + +""" + uuid_string(id::UUID = uuid()) -> Dict{Int, String} + uuid_string(id::UUID = uuid(), T) -> T{Int, String} where T <: AbstractDict + uuid_string(id::UUID = uuid(), fmt::Int) -> String + +# Examples +```jldoctest +julia> uuid_string(OrderedDict, "123e4567-e89b-12d3-a456-426614174000") +OrderedDict{Int64, String} with 7 entries: + 22 => "0YQJpYwUwvbaLOwTUr4thA" + 24 => "0YQJpYw-UwvbaLO-wTUr4thA" + 25 => "12vqjrnxk8whv3i8qi6qgrlz4" + 29 => "12vqj-rnxk8-whv3i-8qi6q-grlz4" + 32 => "123e4567e89b12d3a456426614174000" + 36 => "123e4567-e89b-12d3-a456-426614174000" + 39 => "123e-4567-e89b-12d3-a456-4266-1417-4000" +``` +""" +function uuid_string end +function uuid_string(::Type{T}, id::Any) where T <: AbstractDict + uuid_string(uuid_parse(id)[end], T) +end +function uuid_string(id::Any, ::Type{T} = Dict) where T <: AbstractDict + uuid_string(uuid_parse(id)[end], T) +end +function uuid_string(::Type{T}, id::UUID = uuid()) where T <: AbstractDict + uuid_string(id, T) +end +function uuid_string(id::UUID = uuid(), ::Type{T} = Dict) where T <: AbstractDict + id36 = string(id) + id22 = string(id.value, base = 62, pad = 22) + id25 = string(id.value, base = 36, pad = 25) + id32 = string(id.value, base = 16, pad = 32) + id24 = replace(id22, r"(.{7})" => s"\1-", count = 2) + id29 = replace(id25, r"(.{5})" => s"\1-", count = 4) + id39 = replace(id32, r"(.{4})" => s"\1-", count = 7) + T(22 => id22, 24 => id24, 25 => id25, 29 => id29, 32 => id32, 36 => id36, 39 => id39) +end +function uuid_string(fmt::Number, id::Any)::String + uuid_string(uuid_parse(id)[end], Int(fmt)) +end +function uuid_string(id::Any, fmt::Number)::String + uuid_string(uuid_parse(id)[end], Int(fmt)) +end +function uuid_string(fmt::Int, id::UUID = uuid())::String + uuid_string(id, fmt) +end +function uuid_string(id::UUID, fmt::Int)::String + if 0 ≥ fmt + argumenterror("Invalid format `$fmt` (should be positive)") + elseif fmt ≡ 36 + string(id) + elseif fmt ≡ 22 + string(id.value, base = 62, pad = fmt) + elseif fmt ≡ 25 + string(id.value, base = 36, pad = fmt) + elseif fmt ≡ 32 + string(id.value, base = 16, pad = fmt) + elseif fmt ≡ 24 + replace(uuid_string(id, 22), r"(.{7})" => s"\1-", count = fmt - 22) + elseif fmt ≡ 29 + replace(uuid_string(id, 25), r"(.{5})" => s"\1-", count = fmt - 25) + elseif fmt ≡ 39 + replace(uuid_string(id, 32), r"(.{4})" => s"\1-", count = fmt - 32) + else + argumenterror("Invalid format `$fmt` (undefined)") + end +end + +""" + uuid_version(id::String) -> Int + uuid_version(id::UUID) -> Int + +Inspect the given UUID or UUID string and return its version (see [RFC +4122](https://www.ietf.org/rfc/rfc4122)). + +# Examples +```jldoctest +julia> uuid_version(uuid()) +4 +``` +""" +function uuid_version end +uuid_version(id::Any)::Int = uuid_version(String(id)) +uuid_version(id::String)::Int = uuid_version(uuid_parse(id)[end]) +uuid_version(id::UUID)::Int = Int(id.value >> 76 & 0xf) + +@noinline argumenterror(msg::AbstractString) = throw(ArgumentError(msg)) + +end # module + diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..ff26c7f --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,79 @@ +# Copyright (C) 2022-2023 Heptazhou +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +using Random: Random +using Test, UUID4 + +u4 = uuid4() +@test 4 == uuid_version(u4) +@test 4 == uuid_version(u4 |> string) +@test 4 == uuid_version(u4 |> string |> GenericString) +@test 4 == uuid_version(u4 |> string |> s -> replace(s, "-" => "")) +@test u4 == UUID(string(u4)) == UUID(GenericString(string(u4))) +@test u4 == UUID(UInt128(u4)) +@test uuid4(MersenneTwister(0)) == uuid4(MersenneTwister(0)) +@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-446655440000") +@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-44665544000098") +@test_throws ArgumentError UUID("z50e8400-e29b-41d4-a716-446655440000") + +# https://github.com/JuliaLang/julia/issues/35860 +Random.seed!(Random.GLOBAL_RNG, 10) +@sync u4 = uuid4() +Random.seed!(Random.GLOBAL_RNG, 10) +@test u4 ≠ uuid4() + +str = "22b4a8a1-e548-4eeb-9270-60426d66a48e" +@test_throws ArgumentError UUID("22b4a8a1ae548-4eeb-9270-60426d66a48e") +@test_throws ArgumentError UUID("22b4a8a1-e548a4eeb-9270-60426d66a48e") +@test_throws ArgumentError UUID("22b4a8a1-e548-4eeba9270-60426d66a48e") +@test_throws ArgumentError UUID("22b4a8a1-e548-4eeb-9270a60426d66a48e") +@test UUID(uppercase(str)) == UUID(str) +for r in (rand(UInt128, 10^3)) + @test UUID(r) == UUID(string(UUID(r))) +end + +fmt = [22, 24, 25, 29, 32, 36, 39] +vec = [ + fmt[1] => "50XjbNooVpOszESTWcsJDk" + fmt[2] => "50XjbNo-oVpOszE-STWcsJDk" + fmt[3] => "9qr1zsf8wf3fn8st1t5r8hh1s" + fmt[4] => "9qr1z-sf8wf-3fn8s-t1t5r-8hh1s" + fmt[5] => "a4929835c612495983c50ac8e9265490" + fmt[6] => "a4929835-c612-4959-83c5-0ac8e9265490" + fmt[7] => "a492-9835-c612-4959-83c5-0ac8-e926-5490" +] +d_o, d_u = OrderedDict(vec), Dict(vec) +u = UUID(d_u[36]) +s = GenericString(string(u)) + +@test uuid_parse(s) == uuid_parse(u) == (36, u) +@test uuid_formats() == d_o.keys == fmt +for n in fmt + @test (n, u) == uuid_parse(d_u[n]) == uuid_parse(d_u[n] |> GenericString) + @test d_u[n] == uuid_string(n, u) == uuid_string(u, n |> UInt32) == d_o[n] + @test d_o[n] == uuid_string(n, u |> string) == uuid_string(u |> string, n) + @test n == uuid_parse(uuid_string(n))[1] +end + +@test_throws ArgumentError uuid_parse(d_u[32], fmt = -1) +@test_throws ArgumentError uuid_parse(d_u[32], fmt = 42) +@test_throws ArgumentError uuid_parse(d_u[32]^2) + +@test uuid_string(u, OrderedDict) == uuid_string(OrderedDict, u) +@test uuid_string(u) == uuid_string(Dict, u) == d_u == Dict(d_o) +@test uuid_string(u) == uuid_string(Dict, s) == uuid_string(s, Dict) +@test_throws ArgumentError uuid_string(u, -1) +@test_throws ArgumentError uuid_string(u, 42) +