Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Make ZonedDateTime an isbits type #287

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ deps/compiled
deps/local
!deps/local/windowsZones.xml
deps/active_version
deps/latest
test/tzsource
docs/build

Expand Down
1 change: 1 addition & 0 deletions src/TimeZones.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
15 changes: 15 additions & 0 deletions src/external.jl
Original file line number Diff line number Diff line change
@@ -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]
42 changes: 41 additions & 1 deletion src/types/fixedtimezone.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

"""
Expand All @@ -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)

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions src/types/timezone.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const TIME_ZONE_CACHE = Dict{String,Tuple{TimeZone,Class}}()
const _TIME_ZONES = ExternalField{TimeZone}()

"""
TimeZone(str::AbstractString) -> TimeZone
Expand Down
32 changes: 30 additions & 2 deletions src/types/variabletimezone.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 32 additions & 4 deletions src/types/zoneddatetime.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,50 @@ 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)
if timezone.cutoff !== nothing && utc_datetime >= timezone.cutoff
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

Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Mocking

using RecipesBase
using Serialization
using Test
using TimeZones
using TimeZones: PKG_DIR
Expand Down
12 changes: 11 additions & 1 deletion test/types/zoneddatetime.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a test that emulates, or just plain is, another process with a different set of globals.
This looks like it handles that right to me, but let's have tests to prove it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can temporarily clear the globals for the tests

end
end