Skip to content

Commit

Permalink
Re-add support for parsing/formatting in Julia 0.6
Browse files Browse the repository at this point in the history
Changes work in conjunction with:

JuliaLang/julia#20952
  • Loading branch information
omus committed Mar 17, 2017
1 parent 2b27880 commit 8cdb4a6
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 78 deletions.
3 changes: 2 additions & 1 deletion src/Olson.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ function parsedate(s::AbstractString)
format = join(split("yyyy uuu dd HH:MM:SS", " ")[1:num_periods], ' ')
periods = parse_components(s, DateFormat(format))

# Deal with zone "Pacific/Apia" which has a 24:00 datetime.
# Roll over 24:00 to the next day which occurs in "Pacific/Apia".
# Not a general purpose solution. For example won't work at the end of the month.
if length(periods) > 3 && periods[4] == Hour(24)
periods[4] = Hour(0)
periods[3] += Day(1)
Expand Down
62 changes: 40 additions & 22 deletions src/TimeZones.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,48 @@ if is_windows()
end

function __init__()
if VERSION < v"0.6.0-dev.2307"
# SLOT_RULE extension needs to happen everytime the module is loaded (issue #24)
# Base extension needs to happen everytime the module is loaded (issue #24)
if isdefined(Base.Dates, :SLOT_RULE)
Base.Dates.SLOT_RULE['z'] = TimeZone
Base.Dates.SLOT_RULE['Z'] = TimeZone
else
Base.Dates.CONVERSION_SPECIFIERS['z'] = TimeZone
Base.Dates.CONVERSION_SPECIFIERS['Z'] = TimeZone
Base.Dates.CONVERSION_DEFAULTS[TimeZone] = ""
Base.Dates.CONVERSION_TRANSLATIONS[ZonedDateTime] = (
Year, Month, Day, Hour, Minute, Second, Millisecond, TimeZone,
)
end

global const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz")
end

"""
TimeZone(str::AbstractString) -> TimeZone
Constructs a `TimeZone` subtype based upon the string. If the string is a recognized time
zone name then data is loaded from the compiled IANA time zone database. Otherwise the
string is assumed to be a static time zone.
global const ISOZonedDateTimeFormat = DateFormat("yyyy-mm-ddTHH:MM:SS.ssszzz")
A list of recognized time zones names is available from `timezone_names()`. Supported static
time zone string formats can be found in `FixedTimeZone(::AbstractString)`.
"""
function TimeZone(str::AbstractString)
return get!(TIME_ZONES, str) do
tz_path = joinpath(COMPILED_DIR, split(str, "/")...)

# Only parse string as an explicit FixedTimeZone if there is no file to load
if !isfile(tz_path)
try
return FixedTimeZone(str)
catch
throw(ArgumentError("Unknown time zone named $str"))
end
end

open(tz_path, "r") do fp
return deserialize(fp)
end
end
end

Expand All @@ -59,24 +95,6 @@ include("local.jl")
include("ranges.jl")
include("discovery.jl")
VERSION >= v"0.5.0-dev+5244" && include("rounding.jl")

"""
TimeZone(name::AbstractString) -> TimeZone
Constructs a `TimeZone` instance based upon its `name`. A list of available time zones can
be determined using `timezone_names()`.
See `FixedTimeZone(::AbstractString)` for making a custom `TimeZone` instances.
"""
function TimeZone(name::AbstractString)
return get!(TIME_ZONES, name) do
tz_path = joinpath(COMPILED_DIR, split(name, "/")...)
isfile(tz_path) || throw(ArgumentError("Unknown time zone named $name"))

open(tz_path, "r") do fp
return deserialize(fp)
end
end
end
VERSION < v"0.6.0-dev.2307" ? include("parse-old.jl") : include("parse.jl")

end # module
41 changes: 0 additions & 41 deletions src/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,44 +55,3 @@ function show(io::IO,tz::VariableTimeZone)
end

show(io::IO,dt::ZonedDateTime) = print(io, string(dt))

# DateTime Parsing
if VERSION < v"0.6.0-dev.2307"
import Base.Dates: Slot, slotparse, slotformat
function slotparse(slot::Slot{TimeZone},x,locale)
if slot.letter == 'z'
# TODO: Should 'z' only parse numeric UTC offsets? e.g. disallow "UTC-7"
if ismatch(r"\d", x)
return FixedTimeZone(x)
else
throw(ArgumentError("Time zone offset contains no digits"))
end
elseif slot.letter == 'Z'
# First attempt to create a timezone from the string. An error will be thrown if the
# time zone is unrecognized.
tz = TimeZone(x)

# If the time zone is recognized make sure that it is well-defined. For our purposes
# we'll treat all abbreviations except for UTC and GMT as ambiguous.
# e.g. "MST": "Mountain Standard Time" (UTC-7) or "Moscow Summer Time" (UTC+3:31).
if contains(x, "/") || x in ("UTC", "GMT")
return tz
else
throw(ArgumentError("Time zone is ambiguous"))
end
end
end

function slotformat(slot::Slot{TimeZone},zdt::ZonedDateTime,locale)
if slot.letter == 'z'
return string(zdt.zone.offset)
elseif slot.letter == 'Z'
return string(zdt.zone) # In most cases will be an abbreviation.
end
end

# Note: ISOZonedDateTimeFormat is defined in the module __init__ which means that this
# function can not be called from within this module. TODO: Ignore linting for this line
ZonedDateTime(dt::AbstractString,df::DateFormat=ISOZonedDateTimeFormat) = ZonedDateTime(Base.Dates.parse(dt,df)...)
ZonedDateTime(dt::AbstractString,format::AbstractString;locale::AbstractString="english") = ZonedDateTime(dt,DateFormat(format,locale))
end
47 changes: 47 additions & 0 deletions src/parse-old.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Base: parse
import Base.Dates: Slot, slotparse, slotformat

function slotparse(slot::Slot{TimeZone},x,locale)
if slot.letter == 'z'
# TODO: Should 'z' only parse numeric UTC offsets? e.g. disallow "UTC-7"
if ismatch(r"\d", x)
return FixedTimeZone(x)
else
throw(ArgumentError("Time zone offset contains no digits"))
end
elseif slot.letter == 'Z'
# First attempt to create a timezone from the string. An error will be thrown if the
# time zone is unrecognized.
tz = TimeZone(x)

# If the time zone is recognized make sure that it is well-defined. For our purposes
# we'll treat all abbreviations except for UTC and GMT as ambiguous.
# e.g. "MST": "Mountain Standard Time" (UTC-7) or "Moscow Summer Time" (UTC+3:31).
if contains(x, "/") || x in ("UTC", "GMT")
return tz
else
throw(ArgumentError("Time zone is ambiguous"))
end
end
end

function slotformat(slot::Slot{TimeZone},zdt::ZonedDateTime,locale)
if slot.letter == 'z'
return string(zdt.zone.offset)
elseif slot.letter == 'Z'
return string(zdt.zone) # In most cases will be an abbreviation.
end
end

function parse(::Type{ZonedDateTime}, str::AbstractString, df::DateFormat)
ZonedDateTime(Base.Dates.parse(str, df)...)
end

# Note: ISOZonedDateTimeFormat is defined in the module __init__ which means that this
# function can not be called from within this module. TODO: Ignore linting for this line
function ZonedDateTime(str::AbstractString, df::DateFormat=ISOZonedDateTimeFormat)
parse(ZonedDateTime, str, df)
end
function ZonedDateTime(str::AbstractString, format::AbstractString; locale::AbstractString="english")
parse(ZonedDateTime, str, DateFormat(format, locale))
end
88 changes: 88 additions & 0 deletions src/parse.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Base.Dates: DateFormat, DatePart, tryparsenext, format, min_width, max_width

function tryparsenext_fixedtz(str, i, len, min_width::Int=1, max_width::Int=0)
tz_start, tz_end = i, 0
min_pos = min_width <= 0 ? i : i + min_width - 1
max_pos = max_width <= 0 ? len : min(chr2ind(str, ind2chr(str,i) + max_width - 1), len)
state = 1
@inbounds while i <= max_pos
c, ii = next(str, i)
if state == 1 && (c == '-' || c == '+')
state = 2
tz_end = i
elseif (state == 1 || state == 2) && '0' <= c <= '9'
state = 3
tz_end = i
elseif state == 3 && c == ':'
state = 4
tz_end = i
elseif (state == 3 || state == 4) && '0' <= c <= '9'
tz_end = i
else
break
end
i = ii
end

if tz_end <= min_pos
return Nullable{String}(), i
else
tz = SubString(str, tz_start, tz_end)
return Nullable{String}(tz), i
end
end

function tryparsenext_tz(str, i, len, min_width::Int=1, max_width::Int=0)
tz_start, tz_end = i, 0
min_pos = min_width <= 0 ? i : i + min_width - 1
max_pos = max_width <= 0 ? len : min(chr2ind(str, ind2chr(str,i) + max_width - 1), len)
@inbounds while i <= max_pos
c, ii = next(str, i)
if c == '/' || c == '_' || isalpha(c)
tz_end = i
else
break
end
i = ii
end

if tz_end == 0
return Nullable{String}(), i
else
name = SubString(str, tz_start, tz_end)

# If the time zone is recognized make sure that it is well-defined. For our
# purposes we'll treat all abbreviations except for UTC and GMT as ambiguous.
# e.g. "MST": "Mountain Standard Time" (UTC-7) or "Moscow Summer Time" (UTC+3:31).
if contains(name, "/") || name in ("UTC", "GMT")
return Nullable{String}(name), i
else
return Nullable{String}(), i
end
end
end

function tryparsenext(d::DatePart{'z'}, str, i, len)
tryparsenext_fixedtz(str, i, len, min_width(d), max_width(d))
end

function tryparsenext(d::DatePart{'Z'}, str, i, len)
tryparsenext_tz(str, i, len, min_width(d), max_width(d))
end

function format(io::IO, d::DatePart{'z'}, zdt, locale)
write(io, string(zdt.zone.offset))
end

function format(io::IO, d::DatePart{'Z'}, zdt, locale)
write(io, string(zdt.zone)) # In most cases will be an abbreviation.
end

# Note: ISOZonedDateTimeFormat is defined in the module __init__ which means that this
# function can not be called from within this module. TODO: Ignore linting for this line
function ZonedDateTime(str::AbstractString, df::DateFormat=ISOZonedDateTimeFormat)
parse(ZonedDateTime, str, df)
end
function ZonedDateTime(str::AbstractString, format::AbstractString; locale::AbstractString="english")
ZonedDateTime(str, DateFormat(format,locale))
end
6 changes: 6 additions & 0 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ end
ZonedDateTime(DateTime(y,m,d,h,mi,s,ms), tz)
end

# Parsing constructor. Note we typically don't support passing in time zone information as a
# string since we cannot do not know if we need to support resolving ambiguity.
function ZonedDateTime(y::Int64, m::Int64, d::Int64, h::Int64, mi::Int64, s::Int64, ms::Int64, tz::String)
ZonedDateTime(DateTime(y,m,d,h,mi,s,ms), TimeZone(tz))
end


function ZonedDateTime(parts::Union{Period,TimeZone}...)
periods = Period[]
Expand Down
35 changes: 21 additions & 14 deletions test/io.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import TimeZones.Olson: parse_components

null = FixedTimeZone("", 10800)
fixed = FixedTimeZone("UTC+01:00")
est = FixedTimeZone("EST", -18000)
warsaw = resolve("Europe/Warsaw", tzdata["europe"]...)
apia = resolve("Pacific/Apia", tzdata["australasia"]...)
honolulu = resolve("Pacific/Honolulu", tzdata["northamerica"]...) # Uses cutoff
ulyanovsk = resolve("Europe/Ulyanovsk", tzdata["europe"]...) # No named abbreviations
new_york = resolve("America/New_York", tzdata["northamerica"]...) # Underscore in name
dt = DateTime(1942,12,25,1,23,45)

buffer = IOBuffer()
Expand Down Expand Up @@ -65,27 +68,31 @@ show(buffer, zdt)


# TimeZone parsing
if VERSION < v"0.6.0-dev.2307"

# Make sure that TimeZone conversion specifiers are set (issue #24)
CONVERSION_SPECIFIERS = isdefined(Base.Dates, :SLOT_RULE) ? Dates.keys(Dates.SLOT_RULE) : keys(Dates.CONVERSION_SPECIFIERS)
@test 'z' in CONVERSION_SPECIFIERS
@test 'Z' in CONVERSION_SPECIFIERS

df = Dates.DateFormat("z")
@test !isempty(df.slots) # Ensure that 'z' slot character is recognized (issue #24)
@test Dates.parse("+0100", df) == Any[FixedTimeZone("+01:00")]
@test_throws ArgumentError Dates.parse("EST", df)
@test_throws ArgumentError Dates.parse("UTC", df)
@test_throws ArgumentError Dates.parse("Europe/Warsaw", df)
@test parse_components("+0100", df) == Any[FixedTimeZone("+01:00")]
@test_throws ArgumentError parse_components("EST", df)
@test_throws ArgumentError parse_components("UTC", df)
@test_throws ArgumentError parse_components("Europe/Warsaw", df)

df = Dates.DateFormat("Z")
@test !isempty(df.slots) # Ensure that 'Z' slot character is recognized (issue #24)
@test_throws ArgumentError Dates.parse("+0100", df)
@test_throws ArgumentError Dates.parse("EST", df)
@test Dates.parse("UTC", df) == Any[FixedTimeZone("UTC")]
@test Dates.parse("Europe/Warsaw", df) == Any[warsaw]
@test_throws ArgumentError parse_components("+0100", df)
@test_throws ArgumentError parse_components("EST", df)
@test parse_components("UTC", df) == Any[FixedTimeZone("UTC")]
@test parse_components("Europe/Warsaw", df) == Any[warsaw]

# ZonedDateTime parsing.
# Note: uses compiled time zone information. If these tests are failing try to rebuild
# the TimeZones package.
@test ZonedDateTime("1942-12-25T01:23:45.000+01:00") == ZonedDateTime(dt, fixed)
@test ZonedDateTime("1942-12-25T01:23:45+0100", "yyyy-mm-ddTHH:MM:SSzzz") == ZonedDateTime(dt, fixed)
@test ZonedDateTime("1942-12-25T01:23:45 Europe/Warsaw", "yyyy-mm-ddTHH:MM:SS ZZZ") == ZonedDateTime(dt, warsaw)
@test ZonedDateTime("1942-12-25T01:23:45 America/New_York", "yyyy-mm-ddTHH:MM:SS ZZZ") == ZonedDateTime(dt, new_york)

x = "1942-12-25T01:23:45.123+01:00"
@test string(ZonedDateTime(x)) == x
Expand Down Expand Up @@ -113,6 +120,6 @@ f = "yyyy/m/d H:M:S zzz"
# The "Z" slot displays the time zone abbreviation for VariableTimeZones. It is fine to use
# the abbreviation for display purposes but not fine for parsing. This means that we
# currently cannot parse all strings produced by format.
f = Dates.DateFormat("yyyy-mm-ddTHH:MM:SS ZZZ")
@test_throws ArgumentError Dates.parse(Dates.format(ZonedDateTime(dt, warsaw), f), f)
end
df = Dates.DateFormat("yyyy-mm-ddTHH:MM:SS ZZZ")
zdt = ZonedDateTime(dt, warsaw)
@test_throws ArgumentError parse(ZonedDateTime, Dates.format(zdt, df), df)

0 comments on commit 8cdb4a6

Please sign in to comment.