diff --git a/.gitignore b/.gitignore index 6c23a2d20..c16d962a0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ deps/compiled deps/local !deps/local/windowsZones.xml deps/active_version +deps/latest test/tzsource docs/build diff --git a/src/TimeZones.jl b/src/TimeZones.jl index 754cecf58..3c2500d10 100644 --- a/src/TimeZones.jl +++ b/src/TimeZones.jl @@ -55,6 +55,7 @@ include("indexable_generator.jl") include("class.jl") include("utcoffset.jl") +include("external.jl") include(joinpath("types", "timezone.jl")) include(joinpath("types", "fixedtimezone.jl")) include(joinpath("types", "variabletimezone.jl")) diff --git a/src/external.jl b/src/external.jl new file mode 100644 index 000000000..f01d2a92b --- /dev/null +++ b/src/external.jl @@ -0,0 +1,15 @@ +struct ExternalField{T} + table::Dict{T,Int} + data::Vector{T} +end + +ExternalField{T}() where T = ExternalField{T}(Dict{T,Int}(), Vector{T}()) + +function add!(x::ExternalField{T}, val::T) where T + get!(x.table, val) do + push!(x.data, val) + lastindex(x.data) + end +end + +Base.getindex(x::ExternalField, i::Int) = x.data[i] diff --git a/src/types/fixedtimezone.jl b/src/types/fixedtimezone.jl index 1d349ca07..dc928f0d1 100644 --- a/src/types/fixedtimezone.jl +++ b/src/types/fixedtimezone.jl @@ -29,9 +29,20 @@ const FIXED_TIME_ZONE_REGEX = r""" A `TimeZone` with a constant offset for all of time. """ -struct FixedTimeZone <: TimeZone +mutable struct FixedTimeZone <: TimeZone name::String offset::UTCOffset + index::Int + + function FixedTimeZone(name::String, utc_offset::UTCOffset) + tz = new(name, utc_offset) + tz.index = add!(_TIME_ZONES, tz) + return tz + end +end + +function FixedTimeZone(name::AbstractString, utc_offset::UTCOffset) + FixedTimeZone(convert(String, name), utc_offset) end """ @@ -48,6 +59,21 @@ function FixedTimeZone(name::AbstractString, utc_offset::Second, dst_offset::Sec FixedTimeZone(name, UTCOffset(utc_offset, dst_offset)) end +# Overload serialization to ensure that `FixedTimeZone` serialization doesn't transfer +# state information which is specific to the current Julia process. +function Serialization.serialize(s::AbstractSerializer, tz::FixedTimeZone) + Serialization.serialize_type(s, typeof(tz)) + serialize(s, tz.name) + serialize(s, tz.offset) +end + +function Serialization.deserialize(s::AbstractSerializer, ::Type{FixedTimeZone}) + name = deserialize(s) + offset = deserialize(s) + + return FixedTimeZone(name, offset) +end + # https://en.wikipedia.org/wiki/ISO_8601#Coordinated_Universal_Time_(UTC) const UTC_ZERO = FixedTimeZone("Z", 0) @@ -95,3 +121,17 @@ end name(tz::FixedTimeZone) = tz.name rename(tz::FixedTimeZone, name::AbstractString) = FixedTimeZone(name, tz.offset) + +function Base.:(==)(a::FixedTimeZone, b::FixedTimeZone) + return a.name == b.name && a.offset == b.offset +end + +function Base.hash(tz::FixedTimeZone, h::UInt) + h = hash(:timezone, h) + h = hash(tz.name, h) + return h +end + +function Base.isequal(a::FixedTimeZone, b::FixedTimeZone) + return isequal(a.name, b.name) && isequal(a.offset, b.offset) +end diff --git a/src/types/timezone.jl b/src/types/timezone.jl index 7e8289b1f..0ac199e1c 100644 --- a/src/types/timezone.jl +++ b/src/types/timezone.jl @@ -1,4 +1,5 @@ const TIME_ZONE_CACHE = Dict{String,Tuple{TimeZone,Class}}() +const _TIME_ZONES = ExternalField{TimeZone}() """ TimeZone(str::AbstractString) -> TimeZone diff --git a/src/types/variabletimezone.jl b/src/types/variabletimezone.jl index 35b2b89b6..628c08ff4 100644 --- a/src/types/variabletimezone.jl +++ b/src/types/variabletimezone.jl @@ -5,21 +5,49 @@ end Base.isless(a::Transition, b::Transition) = isless(a.utc_datetime, b.utc_datetime) +function Base.:(==)(a::Transition, b::Transition) + return a.utc_datetime == b.utc_datetime && a.zone == b.zone +end + +function Base.isequal(a::Transition, b::Transition) + return isequal(a.utc_datetime, b.utc_datetime) && isequal(a.zone, b.zone) +end + """ VariableTimeZone A `TimeZone` with an offset that changes over time. """ -struct VariableTimeZone <: TimeZone +mutable struct VariableTimeZone <: TimeZone name::String transitions::Vector{Transition} cutoff::Union{DateTime,Nothing} + index::Int function VariableTimeZone(name::AbstractString, transitions::Vector{Transition}, cutoff::Union{DateTime,Nothing}=nothing) - new(name, transitions, cutoff) + tz = new(name, transitions, cutoff) + tz.index = add!(_TIME_ZONES, tz) + return tz end end +# Overload serialization to ensure that `VariableTimeZone` serialization doesn't transfer +# state information which is specific to the current Julia process. +function Serialization.serialize(s::AbstractSerializer, tz::VariableTimeZone) + Serialization.serialize_type(s, typeof(tz)) + serialize(s, tz.name) + serialize(s, tz.transitions) + serialize(s, tz.cutoff) +end + +function Serialization.deserialize(s::AbstractSerializer, ::Type{VariableTimeZone}) + name = deserialize(s) + transitions = deserialize(s) + cutoff = deserialize(s) + + return VariableTimeZone(name, transitions, cutoff) +end + name(tz::VariableTimeZone) = tz.name function rename(tz::VariableTimeZone, name::AbstractString) diff --git a/src/types/zoneddatetime.jl b/src/types/zoneddatetime.jl index 2c832e8d9..524225d51 100644 --- a/src/types/zoneddatetime.jl +++ b/src/types/zoneddatetime.jl @@ -8,11 +8,11 @@ using Dates: AbstractDateTime, argerror, validargs struct ZonedDateTime <: AbstractDateTime utc_datetime::DateTime - timezone::TimeZone - zone::FixedTimeZone # The current zone for the utc_datetime. + _tz_index::Int + _zone_index::Int function ZonedDateTime(utc_datetime::DateTime, timezone::TimeZone, zone::FixedTimeZone) - return new(utc_datetime, timezone, zone) + return new(utc_datetime, timezone.index, zone.index) end function ZonedDateTime(utc_datetime::DateTime, timezone::VariableTimeZone, zone::FixedTimeZone) @@ -20,10 +20,38 @@ struct ZonedDateTime <: AbstractDateTime throw(UnhandledTimeError(timezone)) end - return new(utc_datetime, timezone, zone) + return new(utc_datetime, timezone.index, zone.index) end end +function Base.getproperty(zdt::ZonedDateTime, field::Symbol) + if field === :zone + return _TIME_ZONES[getfield(zdt, :_zone_index)]::FixedTimeZone + elseif field === :timezone + return _TIME_ZONES[getfield(zdt, :_tz_index)]::TimeZone + else + return getfield(zdt, field) + end +end + +# Overload serialization to ensure that `ZonedDateTime` serialization doesn't transfer +# state information which is specific to the current Julia process. +function Serialization.serialize(s::AbstractSerializer, zdt::ZonedDateTime) + Serialization.serialize_type(s, typeof(zdt)) + serialize(s, zdt.utc_datetime) + serialize(s, zdt.timezone) + serialize(s, zdt.zone) +end + +function Serialization.deserialize(s::AbstractSerializer, ::Type{ZonedDateTime}) + utc_datetime = deserialize(s) + timezone = deserialize(s) + zone = deserialize(s) + + return ZonedDateTime(utc_datetime, timezone, zone) +end + + """ ZonedDateTime(dt::DateTime, tz::TimeZone; from_utc=false) -> ZonedDateTime diff --git a/test/runtests.jl b/test/runtests.jl index b631e70f7..f5492c84c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using Mocking using RecipesBase +using Serialization using Test using TimeZones using TimeZones: PKG_DIR diff --git a/test/types/zoneddatetime.jl b/test/types/zoneddatetime.jl index e776e5bb9..efbf5ae42 100644 --- a/test/types/zoneddatetime.jl +++ b/test/types/zoneddatetime.jl @@ -340,7 +340,7 @@ using Dates: Hour, Second, UTM, @dateformat_str y = deepcopy(x) @test x == y - @test x !== y + # @test x !== y @test !(x < y) @test !(x > y) @test isequal(x, y) @@ -430,4 +430,14 @@ using Dates: Hour, Second, UTM, @dateformat_str @test typemin(ZonedDateTime) <= ZonedDateTime(typemin(DateTime), utc) @test typemax(ZonedDateTime) >= ZonedDateTime(typemax(DateTime), utc) end + + @testset "serialization" begin + zdt = ZonedDateTime(2020, 9, 1, warsaw) + + b = IOBuffer() + serialize(Serializer(b), zdt) + seekstart(b) + + @test deserialize(Serializer(b)) == zdt + end end