diff --git a/src/TimeZones.jl b/src/TimeZones.jl index 9e83cc450..6c30e8fbc 100644 --- a/src/TimeZones.jl +++ b/src/TimeZones.jl @@ -55,6 +55,7 @@ include("indexable_generator.jl") include("class.jl") include("utcoffset.jl") +include(joinpath("types", "tzabbr.jl")) include(joinpath("types", "timezone.jl")) include(joinpath("types", "fixedtimezone.jl")) include(joinpath("types", "variabletimezone.jl")) diff --git a/src/types/tzabbr.jl b/src/types/tzabbr.jl new file mode 100644 index 000000000..a28160c41 --- /dev/null +++ b/src/types/tzabbr.jl @@ -0,0 +1,89 @@ +const TZ_ABBR_MAX = 6 + +""" + TimeZoneAbbr + +A three to six character string containing only ASCII alphanumerics, '+', and '-'. + +# https://data.iana.org/time-zones/theory.html#abbreviations +""" +struct TZAbbr <: AbstractString + chars::NTuple{TZ_ABBR_MAX, UInt8} +end + +function TZAbbr(str::AbstractString) + if length(str) > TZ_ABBR_MAX + throw(ArgumentError("String length ($(length(str)) exceeds maximum abbreviation length ($TZ_ABBR_MAX)")) + end + chars = fill(tz_abbr_encode('\0'), TZ_ABBR_MAX) + i = 1 + for c in str + chars[i] = tz_abbr_encode(c) + i += 1 + end + return TZAbbr(Tuple(chars)) +end + +Base.iterate(a::TZAbbr) = iterate(a, 1) + +function Base.iterate(a::TZAbbr, i::Int) + i > TZ_ABBR_MAX && return nothing + c = tz_abbr_decode(a.chars[i]) + c == '\0' && return nothing + return c, i + 1 +end + +function Base.ncodeunits(a::TZAbbr) + i = 0 + for v in a.chars + c = tz_abbr_decode(v) + c == '\0' && return i + i += 1 + end + return i +end + +Base.codeunit(a::TZAbbr) = UInt8 +Base.codeunit(a::TZAbbr, i::Integer) = tz_abbr_decode(a.chars[i]) +Base.isvalid(a::TZAbbr, i::Integer) = tz_abbr_decode(a.chars[i]) != '\0' +Base.sizeof(::TZAbbr) = TZ_ABBR_MAX + +function tz_abbr_decode(v::UInt8) + c = if v == UInt8(0) + '\0' + elseif v <= UInt8(26) # A-Z + 'A' + v - 1 + elseif v <= UInt8(52) # a-z + 'a' + v - 27 + elseif v <= UInt8(62) # 0-9 + '0' + v - 53 + elseif v == UInt8(63) + '+' + elseif v == UInt8(64) + '-' + else + throw(ArgumentError("Decoding for $(repr(v)) is undefined and reserved for future use")) + end + + return c +end + +function tz_abbr_encode(c::Char) + v = if c == '\0' + UInt8(0) + elseif 'A' <= c <= 'Z' + UInt8(c - 'A' + 1) + elseif 'a' <= c <= 'z' + UInt8(c - 'a' + 27) + elseif '0' <= c <= '9' + UInt8(c - '0' + 53) + elseif c == '+' + UInt8(63) + elseif c == '-' + UInt8(64) + else + throw(ArgumentError("Character $(repr(c)) does not have a defined TZAbbr encoding")) + end + + return v +end diff --git a/test/runtests.jl b/test/runtests.jl index b631e70f7..7c157f057 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -52,6 +52,7 @@ include("helpers.jl") include(joinpath("tzdata", "compile.jl")) Sys.iswindows() && include(joinpath("winzone", "WindowsTimeZoneIDs.jl")) include("utcoffset.jl") + include(joinpath("types", "tzabbr.jl")) include(joinpath("types", "timezone.jl")) include(joinpath("types", "fixedtimezone.jl")) include(joinpath("types", "variabletimezone.jl")) diff --git a/test/types/tzabbr.jl b/test/types/tzabbr.jl new file mode 100644 index 000000000..cbe2e10b2 --- /dev/null +++ b/test/types/tzabbr.jl @@ -0,0 +1,59 @@ +@testset "tz_abbr_encode" begin + @test tz_abbr_encode('\0') == UInt8(0) + @test tz_abbr_encode('A') == UInt8(1) + @test tz_abbr_encode('Z') == UInt8(26) + @test tz_abbr_encode('a') == UInt8(27) + @test tz_abbr_encode('z') == UInt8(52) + @test tz_abbr_encode('0') == UInt8(53) + @test tz_abbr_encode('+') == UInt8(63) + @test tz_abbr_encode('-') == UInt8(64) + @test_throws ArgumentError tz_abbr_encode('?') +end + +@testset "tz_abbr_decode" begin + @test tz_abbr_decode(UInt8(0)) == '\0' + @test tz_abbr_decode(UInt8(1)) == 'A' + @test tz_abbr_decode(UInt8(26)) == 'Z' + @test tz_abbr_decode(UInt8(27)) == 'a' + @test tz_abbr_decode(UInt8(52)) == 'z' + @test tz_abbr_decode(UInt8(53)) == '0' + @test tz_abbr_decode(UInt8(63)) == '+' + @test tz_abbr_decode(UInt8(64)) == '-' + @test_throws ArgumentError tz_abbr_decode(UInt8(65)) +end + +@testset "TZAbbr" begin + @testset "iteration" begin + abbr = TZAbbr("X") + + x = iterate(abbr) + @test x == ('X', 2) + + x = iterate(abbr, 2) + @test x === nothing + + @test collect(TZAbbr("UTC")) == ['U', 'T', 'C'] + end + + @testset "codeunits" begin + abbr = TZAbbr("UTC") + + @test ncodeunits(abbr) == 3 + @test codeunit(abbr) == UInt8 + @test codeunit(abbr, 1) == 'U' + end + + @testset "getindex" begin + abbr = TZAbbr("PDT") + @test abbr[1] == 'P' + @test abbr[2] == 'D' + @test abbr[3] == 'T' + @test_throws BoundsError abbr[4] + end + + @testset "sizeof" begin + @test sizeof(TZAbbr("")) == 6 + @test sizeof(TZAbbr("UTC")) == 6 + @test sizeof(TZAbbr("FOOBAR")) == 6 + end +end