Skip to content

Commit 4b18524

Browse files
committed
Support "slim" zoneinfo files produced by default by zic >= 2020b.
Use the POSIX-style TZ string to calculate transitions after the last defined transition contained within the file. At the moment these transitions are generated when the file is loaded. In a later release this will likely be changed to calculate transitions on demand. Use the 64-bit section of zoneinfo files regardless of whether the runtime supports 64-bit Times. The 32-bit section is empty in "slim" zoneinfo files. Resolves #120.
1 parent 6dfdfc2 commit 4b18524

12 files changed

+2768
-154
lines changed

lib/tzinfo.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module TZInfo
1010

1111
require 'tzinfo/timezone_offset'
1212
require 'tzinfo/timezone_transition'
13+
require 'tzinfo/transition_rule'
14+
require 'tzinfo/annual_rules'
1315
require 'tzinfo/timezone_transition_definition'
1416

1517
require 'tzinfo/timezone_index_definition'
@@ -22,6 +24,7 @@ module TZInfo
2224

2325
require 'tzinfo/data_source'
2426
require 'tzinfo/ruby_data_source'
27+
require 'tzinfo/posix_time_zone_parser'
2528
require 'tzinfo/zoneinfo_data_source'
2629

2730
require 'tzinfo/timezone_period'

lib/tzinfo/annual_rules.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module TZInfo
2+
# A set of rules that define when transitions occur in time zones with
3+
# annually occurring daylight savings time.
4+
#
5+
# @private
6+
class AnnualRules #:nodoc:
7+
# Returned by #transitions. #offset is the TimezoneOffset that applies
8+
# from the UTC TimeOrDateTime #at. #previous_offset is the prior
9+
# TimezoneOffset.
10+
Transition = Struct.new(:offset, :previous_offset, :at)
11+
12+
# The standard offset that applies when daylight savings time is not in
13+
# force.
14+
attr_reader :std_offset
15+
16+
# The offset that applies when daylight savings time is in force.
17+
attr_reader :dst_offset
18+
19+
# The rule that determines when daylight savings time starts.
20+
attr_reader :dst_start_rule
21+
22+
# The rule that determines when daylight savings time ends.
23+
attr_reader :dst_end_rule
24+
25+
# Initializes a new {AnnualRules} instance.
26+
def initialize(std_offset, dst_offset, dst_start_rule, dst_end_rule)
27+
@std_offset = std_offset
28+
@dst_offset = dst_offset
29+
@dst_start_rule = dst_start_rule
30+
@dst_end_rule = dst_end_rule
31+
end
32+
33+
# Returns the transitions between standard and daylight savings time for a
34+
# given year. The results are ordered by time of occurrence (earliest to
35+
# latest).
36+
def transitions(year)
37+
start_dst = apply_rule(@dst_start_rule, @std_offset, @dst_offset, year)
38+
end_dst = apply_rule(@dst_end_rule, @dst_offset, @std_offset, year)
39+
40+
end_dst.at < start_dst.at ? [end_dst, start_dst] : [start_dst, end_dst]
41+
end
42+
43+
private
44+
45+
# Applies a given rule between offsets on a year.
46+
def apply_rule(rule, from_offset, to_offset, year)
47+
at = rule.at(from_offset, year)
48+
Transition.new(to_offset, from_offset, at)
49+
end
50+
end
51+
end

lib/tzinfo/posix_time_zone_parser.rb

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# encoding: UTF-8
2+
# frozen_string_literal: true
3+
4+
require 'strscan'
5+
6+
module TZInfo
7+
# An {InvalidPosixTimeZone} exception is raised if an invalid POSIX-style
8+
# time zone string is encountered.
9+
#
10+
# @private
11+
class InvalidPosixTimeZone < StandardError #:nodoc:
12+
end
13+
14+
# A parser for POSIX-style TZ strings used in zoneinfo files and specified
15+
# by tzfile.5 and tzset.3.
16+
#
17+
# @private
18+
class PosixTimeZoneParser #:nodoc:
19+
# Parses a POSIX-style TZ string, returning either a TimezoneOffset or
20+
# an AnnualRules instance.
21+
def parse(tz_string)
22+
raise InvalidPosixTimeZone unless tz_string.kind_of?(String)
23+
return nil if tz_string.empty?
24+
25+
s = StringScanner.new(tz_string)
26+
check_scan(s, /([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
27+
std_abbrev = s[1] || s[2]
28+
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
29+
std_offset = get_offset_from_hms(s[1], s[2], s[3])
30+
31+
if s.scan(/([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
32+
dst_abbrev = s[1] || s[2]
33+
34+
if s.scan(/([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
35+
dst_offset = get_offset_from_hms(s[1], s[2], s[3])
36+
else
37+
# POSIX is negative for ahead of UTC.
38+
dst_offset = std_offset - 3600
39+
end
40+
41+
dst_difference = std_offset - dst_offset
42+
43+
start_rule = parse_rule(s, 'start')
44+
end_rule = parse_rule(s, 'end')
45+
46+
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." if s.rest?
47+
48+
if start_rule.is_always_first_day_of_year? && start_rule.transition_at == 0 &&
49+
end_rule.is_always_last_day_of_year? && end_rule.transition_at == 86400 + dst_difference
50+
# Constant daylight savings time.
51+
# POSIX is negative for ahead of UTC.
52+
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym)
53+
else
54+
AnnualRules.new(
55+
TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym),
56+
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym),
57+
start_rule,
58+
end_rule)
59+
end
60+
elsif !s.rest?
61+
# Constant standard time.
62+
# POSIX is negative for ahead of UTC.
63+
TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym)
64+
else
65+
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'."
66+
end
67+
end
68+
69+
private
70+
71+
# Parses the rule from the TZ string, returning a TransitionRule.
72+
def parse_rule(s, type)
73+
check_scan(s, /,(?: (?: J(\d+) ) | (\d+) | (?: M(\d+)\.(\d)\.(\d) ) )/x)
74+
julian_day_of_year = s[1]
75+
absolute_day_of_year = s[2]
76+
month = s[3]
77+
week = s[4]
78+
day_of_week = s[5]
79+
80+
if s.scan(/\//)
81+
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
82+
transition_at = get_seconds_after_midnight_from_hms(s[1], s[2], s[3])
83+
else
84+
transition_at = 7200
85+
end
86+
87+
begin
88+
if julian_day_of_year
89+
JulianDayOfYearTransitionRule.new(julian_day_of_year.to_i, transition_at)
90+
elsif absolute_day_of_year
91+
AbsoluteDayOfYearTransitionRule.new(absolute_day_of_year.to_i, transition_at)
92+
elsif week == '5'
93+
LastDayOfMonthTransitionRule.new(month.to_i, day_of_week.to_i, transition_at)
94+
else
95+
DayOfMonthTransitionRule.new(month.to_i, week.to_i, day_of_week.to_i, transition_at)
96+
end
97+
rescue ArgumentError => e
98+
raise InvalidPosixTimeZone, "Invalid #{type} rule in POSIX-style time zone string: #{e}"
99+
end
100+
end
101+
102+
# Returns an offset in seconds from hh:mm:ss values. The value can be
103+
# negative. -02:33:12 would represent 2 hours, 33 minutes and 12 seconds
104+
# ahead of UTC.
105+
def get_offset_from_hms(h, m, s)
106+
h = h.to_i
107+
m = m.to_i
108+
s = s.to_i
109+
raise InvalidPosixTimeZone, "Invalid minute #{m} in offset for POSIX-style time zone string." if m > 59
110+
raise InvalidPosixTimeZone, "Invalid second #{s} in offset for POSIX-style time zone string." if s > 59
111+
magnitude = (h.abs * 60 + m) * 60 + s
112+
h < 0 ? -magnitude : magnitude
113+
end
114+
115+
# Returns the seconds from midnight from hh:mm:ss values. Hours can exceed
116+
# 24 for a time on the following day. Hours can be negative to subtract
117+
# hours from midnight on the given day. -02:33:12 represents 22:33:12 on
118+
# the prior day.
119+
def get_seconds_after_midnight_from_hms(h, m, s)
120+
h = h.to_i
121+
m = m.to_i
122+
s = s.to_i
123+
raise InvalidPosixTimeZone, "Invalid minute #{m} in time for POSIX-style time zone string." if m > 59
124+
raise InvalidPosixTimeZone, "Invalid second #{s} in time for POSIX-style time zone string." if s > 59
125+
(h * 3600) + m * 60 + s
126+
end
127+
128+
# Scans for a pattern and raises an exception if the pattern does not
129+
# match the input.
130+
def check_scan(s, pattern)
131+
result = s.scan(pattern)
132+
raise InvalidPosixTimeZone, "Expected '#{s.rest}' to match #{pattern} in POSIX-style time zone string." unless result
133+
result
134+
end
135+
end
136+
end

lib/tzinfo/time_or_datetime.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ def mday
160160
end
161161
end
162162
alias :day :mday
163+
164+
# Returns the day of the week (0..6 for Sunday to Saturday).
165+
def wday
166+
if @time
167+
@time.wday
168+
elsif @datetime
169+
@datetime.wday
170+
else
171+
to_time.wday
172+
end
173+
end
163174

164175
# Returns the hour of the day (0..23).
165176
def hour

0 commit comments

Comments
 (0)