Skip to content

Commit

Permalink
Merge pull request #163 from JuliaTime/cv/2018h
Browse files Browse the repository at this point in the history
Default to using 2018h tzdata
  • Loading branch information
omus authored Jan 4, 2019
2 parents fa19da8 + 3afadae commit 5fdf306
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 91 deletions.
2 changes: 1 addition & 1 deletion deps/build.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import TimeZones: build
# ENV variable allows users to modify the default to be "latest". Do NOT use "latest"
# as the default here as can make it difficult to debug to past versions of working code.
# Note: Also allows us to only download a single tzdata version during CI jobs.
build(get(ENV, "JULIA_TZ_VERSION", "2018g"))
build(get(ENV, "JULIA_TZ_VERSION", "2018h"))
152 changes: 96 additions & 56 deletions src/tzdata/compile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -69,55 +69,114 @@ const DAYS = Dict(
"Mon" => 1, "Tue" => 2, "Wed" => 3, "Thu" => 4, "Fri" => 5, "Sat" => 6, "Sun" => 7,
)

# Create adjuster functions such as "lastSun".
const LAST_DAY_OF_WEEK = Dict{String, Function}()

# Create adjuster functions such as `last_sunday`.
for (abbr, dayofweek) in DAYS
sym = Symbol("last" * abbr)
@eval (
function $sym(dt)
str = "last" * abbr
f = Symbol("last_" * lowercase(dayname(dayofweek)))
LAST_DAY_OF_WEEK[str] = @eval begin
function $f(dt)
return dayofweek(dt) == $dayofweek &&
dayofweekofmonth(dt) == daysofweekinmonth(dt)
end
)
end
end

# Generate various DateFormats based the number of periods provided.
const UNTIL_FORMATS = let
parts = split("yyyy uuu dd HH:MM:SS", ' ')
map(i -> DateFormat(join(parts[1:i], ' ')), eachindex(parts))
end


isflag(flag::Char) = flag in ('w', 'u', 's')

"""
tryparse_dayofmonth(str::AbstractString) -> Union{Function,Nothing}
Parse the various day-of-month formats used within tzdata source files.
```julia
julia> tryparse_dayofmonth("lastSun")
last_sunday (generic function with 1 method)
julia> tryparse_dayofmonth("Sun>=8")
#15 (generic function with 1 method)
julia> TimeZones.TZData.tryparse_dayofmonth("15")
#16 (generic function with 1 method)
```
"""
function tryparse_dayofmonth(str::AbstractString)
if occursin(r"^last\w{3}$", str)
# We pre-built these functions above
# They follow the format: "lastSun", "lastMon", etc.
LAST_DAY_OF_WEEK[str]
elseif (m = match(r"^(?<dow>\w{3})(?<op>[<>]=)(?<dom>\d{1,2})$", str)) !== nothing
# The first day of the week that occurs before or after a given day of month.
# i.e. Sun>=8 refers to the Sunday after the 8th of the month
# or in other words, the 2nd Sunday.
dow = DAYS[m[:dow]]
dom = parse(Int, m[:dom])
if m[:op] == "<="
dt -> day(dt) <= dom && dayofweek(dt) == dow
else
dt -> day(dt) >= dom && dayofweek(dt) == dow
end
elseif occursin(r"^\d{1,2}$", str)
# Matches just a plain old day of the month
dom = parse(Int, str)
dt -> day(dt) == dom
else
nothing
end
end

# Olson time zone dates can be a single year (1900), yyyy-mm-dd (1900-Jan-01),
# or minute-precision (1900-Jan-01 2:00).
# They can also be given in Local Wall Time, UTC time (u), or Local Standard time (s)
function parsedate(s::AbstractString)
s = replace(s, r"\s+" => " ")
num_periods = length(split(s, " "))
s, flag = num_periods > 3 && isflag(s[end]) ? (s[1:end-1], s[end]) : (s, DEFAULT_FLAG)
if occursin("lastSun", s)
dt = DateTime(replace(s, "lastSun" => "1", count=1), "yyyy uuu d H:MM:SS")
dt = tonext(lastSun, dt; same=true)
elseif occursin("lastSat", s)
dt = DateTime(replace(s, "lastSat" => "1", count=1), "yyyy uuu d H:MM:SS")
dt = tonext(lastSat, dt; same=true)
elseif occursin("Sun>=1", s)
dt = DateTime(replace(s, "Sun>=" => "", count=1), "yyyy uuu d H:MM:SS")
dt = tonext(d -> dayofweek(d) == Sun, dt; same=true)
function parse_date(s::AbstractString)
period_strs = split(s, r"\s+")
num_periods = length(period_strs)

# Extract the flag when time is included
if num_periods > 3 && isflag(s[end])
flag = s[end]
period_strs[end] = period_strs[end][1:end - 1]
else
format = join(split("yyyy uuu dd HH:MM:SS", " ")[1:num_periods], ' ')
periods = parse_components(s, DateFormat(format))

# Roll over 24:00 to the next day which occurs in "Pacific/Apia" and "Asia/Macau".
# Note: shift is applied after we create the DateTime to ensure that roll over works
# correctly at the end of the month or year.
shift = Day(0)
if length(periods) > 3 && periods[4] == Hour(24)
periods[4] = Hour(0)
shift += Day(1)
end
dt = DateTime(periods...) + shift
flag = DEFAULT_FLAG
end

# TODO: I feel like there are issues here.
# If the time is UTC, we add back the offset and any saved amount
# Save the day of month string and substitute a non-numeric string for parsing.
dom_str = num_periods >= 3 ? period_strs[3] : ""
numeric_dom = all(isnumeric, dom_str)
!numeric_dom && splice!(period_strs, 3, ["1"])

periods = parse_components(join(period_strs, ' '), UNTIL_FORMATS[num_periods])

# Roll over 24:00 to the next day which occurs in "Pacific/Apia" and "Asia/Macau".
# Note: Apply the `shift` after we create the DateTime to ensure that roll over works
# correctly at the end of the month or year.
shift = Day(0)
if num_periods > 3 && periods[4] == Hour(24)
periods[4] = Hour(0)
shift += Day(1)
end
dt = DateTime(periods...)

# Adjust the DateTime to reflect the requirements of the day-of-month function.
if !numeric_dom
dom = tryparse_dayofmonth(dom_str)
dom !== nothing || throw(ArgumentError("Unable to parse day-of-month: \"$dom_str\""))
dt = tonext(dom, dt; step=Day(1), same=true)
end

dt += shift

# Note: If the time is UTC, we add back the offset and any saved amount
# If it's local standard time, we just need to add any saved amount
# return letter == 's' ? (dt - save) : (dt - offset - save)

return dt, flag
end

Expand Down Expand Up @@ -164,28 +223,9 @@ function ruleparse(from, to, rule_type, month, on, at, save, letter)

# Now we need to get the right anonymous function
# for determining the right day for transitioning
if occursin(r"last\w\w\w", on)
# We pre-built these functions above
# They follow the format: "lastSun", "lastMon".
on_func = eval(Symbol(on))
elseif occursin(r"\w\w\w[<>]=\d\d?", on)
# The first day of the week that occurs before or after a given day of month.
# i.e. Sun>=8 refers to the Sunday after the 8th of the month
# or in other words, the 2nd Sunday.
dow::Int = DAYS[match(r"\w\w\w", on).match]
dom::Int = parse(Int, match(r"\d\d?", on).match)
if occursin(on, "<=")
on_func = (dt -> day(dt) <= dom && dayofweek(dt) == dow)
else
on_func = (dt -> day(dt) >= dom && dayofweek(dt) == dow)
end
elseif occursin(r"\d\d?", on)
# Matches just a plain old day of the month
dom = parse(Int, on)
on_func = dt -> day(dt) == dom
else
error("Can't parse day of month for DST change")
end
on_func = tryparse_dayofmonth(on)
on_func === nothing && error("Can't parse day of month for DST change: \"$on\"")

# Now we get the time of the transition
c = at[end]
at_hm = TimeOffset(isflag(c) ? at[1:end-1] : at)
Expand Down Expand Up @@ -222,7 +262,7 @@ function zoneparse(gmtoff, rules, format, until="")
format = format == "zzz" ? "" : format

# Parse the date the line rule applies up to
until_tuple = until == "" ? (nothing, 'w') : parsedate(until)
until_tuple = until == "" ? (nothing, 'w') : parse_date(until)
until_dt, until_flag = convert(Nullable{DateTime}, until_tuple[1]), until_tuple[2]

if rules == "-" || occursin(r"\d", rules)
Expand Down
72 changes: 38 additions & 34 deletions test/tzdata/compile.jl
Original file line number Diff line number Diff line change
@@ -1,54 +1,58 @@
import TimeZones: Transition
import TimeZones.TZData: ZoneDict, RuleDict, zoneparse, ruleparse, resolve, parsedate, order_rules
import TimeZones.TZData: ZoneDict, RuleDict, zoneparse, ruleparse, resolve, parse_date, order_rules
import Compat.Dates: Hour, Minute, Second, DateTime, Date

### parsedate ###
### parse_date ###

# Variations of until dates
@test parsedate("1945") == (DateTime(1945), 'w')
@test parsedate("1945 Aug") == (DateTime(1945,8), 'w')
@test parsedate("1945 Aug 2") == (DateTime(1945,8,2), 'w')
@test parsedate("1945 Aug 2 12") == (DateTime(1945,8,2,12), 'w') # Doesn't actually occur
@test parsedate("1945 Aug 2 12:34") == (DateTime(1945,8,2,12,34), 'w')
@test parsedate("1945 Aug 2 12:34:56") == (DateTime(1945,8,2,12,34,56), 'w')
@test parse_date("1945") == (DateTime(1945), 'w')
@test parse_date("1945 Aug") == (DateTime(1945,8), 'w')
@test parse_date("1945 Aug 2") == (DateTime(1945,8,2), 'w')
@test parse_date("1945 Aug 2 12") == (DateTime(1945,8,2,12), 'w') # Doesn't actually occur
@test parse_date("1945 Aug 2 12:34") == (DateTime(1945,8,2,12,34), 'w')
@test parse_date("1945 Aug 2 12:34:56") == (DateTime(1945,8,2,12,34,56), 'w')

# Make sure parsing can handle additional spaces.
@test parsedate("1945 Aug") == (DateTime(1945,8), 'w')
@test parsedate("1945 Aug 2") == (DateTime(1945,8,2), 'w')
@test parsedate("1945 Aug 2 12") == (DateTime(1945,8,2,12), 'w')
@test parsedate("1945 Aug 2 12:34") == (DateTime(1945,8,2,12,34), 'w')
@test parsedate("1945 Aug 2 12:34:56") == (DateTime(1945,8,2,12,34,56), 'w')
@test parse_date("1945 Aug") == (DateTime(1945,8), 'w')
@test parse_date("1945 Aug 2") == (DateTime(1945,8,2), 'w')
@test parse_date("1945 Aug 2 12") == (DateTime(1945,8,2,12), 'w')
@test parse_date("1945 Aug 2 12:34") == (DateTime(1945,8,2,12,34), 'w')
@test parse_date("1945 Aug 2 12:34:56") == (DateTime(1945,8,2,12,34,56), 'w')

# Rollover at the end of the year
@test parse_date("2017 Dec Sun>=31 24:00") == (DateTime(2018,1,1), 'w')

# Explicit zone "local wall time"
@test_throws ArgumentError parsedate("1945w")
@test_throws Exception parsedate("1945 Augw") # Julia <=0.6 KeyError, 0.6 ArgumentError
@test_throws ArgumentError parsedate("1945 Aug 2w")
@test parsedate("1945 Aug 2 12w") == (DateTime(1945,8,2,12), 'w')
@test parsedate("1945 Aug 2 12:34w") == (DateTime(1945,8,2,12,34), 'w')
@test parsedate("1945 Aug 2 12:34:56w") == (DateTime(1945,8,2,12,34,56), 'w')
@test_throws ArgumentError parse_date("1945w")
@test_throws Exception parse_date("1945 Augw") # Julia <=0.6 KeyError, 0.6 ArgumentError
@test_throws ArgumentError parse_date("1945 Aug 2w")
@test parse_date("1945 Aug 2 12w") == (DateTime(1945,8,2,12), 'w')
@test parse_date("1945 Aug 2 12:34w") == (DateTime(1945,8,2,12,34), 'w')
@test parse_date("1945 Aug 2 12:34:56w") == (DateTime(1945,8,2,12,34,56), 'w')

# Explicit zone "UTC time"
@test_throws ArgumentError parsedate("1945u")
@test_throws Exception parsedate("1945 Augu")
@test_throws ArgumentError parsedate("1945 Aug 2u")
@test parsedate("1945 Aug 2 12u") == (DateTime(1945,8,2,12), 'u')
@test parsedate("1945 Aug 2 12:34u") == (DateTime(1945,8,2,12,34), 'u')
@test parsedate("1945 Aug 2 12:34:56u") == (DateTime(1945,8,2,12,34,56), 'u')
@test_throws ArgumentError parse_date("1945u")
@test_throws Exception parse_date("1945 Augu")
@test_throws ArgumentError parse_date("1945 Aug 2u")
@test parse_date("1945 Aug 2 12u") == (DateTime(1945,8,2,12), 'u')
@test parse_date("1945 Aug 2 12:34u") == (DateTime(1945,8,2,12,34), 'u')
@test parse_date("1945 Aug 2 12:34:56u") == (DateTime(1945,8,2,12,34,56), 'u')

# Explicit zone "standard time"
@test_throws ArgumentError parsedate("1945s")
@test_throws Exception parsedate("1945 Augs")
@test_throws ArgumentError parsedate("1945 Aug 2s")
@test parsedate("1945 Aug 2 12s") == (DateTime(1945,8,2,12), 's')
@test parsedate("1945 Aug 2 12:34s") == (DateTime(1945,8,2,12,34), 's')
@test parsedate("1945 Aug 2 12:34:56s") == (DateTime(1945,8,2,12,34,56), 's')
@test_throws ArgumentError parse_date("1945s")
@test_throws Exception parse_date("1945 Augs")
@test_throws ArgumentError parse_date("1945 Aug 2s")
@test parse_date("1945 Aug 2 12s") == (DateTime(1945,8,2,12), 's')
@test parse_date("1945 Aug 2 12:34s") == (DateTime(1945,8,2,12,34), 's')
@test parse_date("1945 Aug 2 12:34:56s") == (DateTime(1945,8,2,12,34,56), 's')

# Invalid zone
@test_throws ArgumentError parsedate("1945 Aug 2 12:34i")
@test_throws ArgumentError parse_date("1945 Aug 2 12:34i")

# Actual dates found tzdata files
@test parsedate("2011 Dec 29 24:00") == (DateTime(2011,12,30), 'w') # Pacific/Apia (2014f)
@test parsedate("1945 Sep 30 24:00") == (DateTime(1945,10,1), 'w') # Asia/Macau (2018f)
@test parse_date("2011 Dec 29 24:00") == (DateTime(2011,12,30), 'w') # Pacific/Apia (2014f)
@test parse_date("1945 Sep 30 24:00") == (DateTime(1945,10,1), 'w') # Asia/Macau (2018f)
@test parse_date("2019 Mar Sun>=8 3:00") == (DateTime(2019,3,10,3), 'w') # America/Metlakatla (2018h)


### order_rules ###
Expand Down

0 comments on commit 5fdf306

Please sign in to comment.