tzinfo 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGES.md +46 -0
- data/README.md +3 -3
- data/lib/tzinfo.rb +3 -0
- data/lib/tzinfo/annual_rules.rb +71 -0
- data/lib/tzinfo/data_sources/posix_time_zone_parser.rb +181 -0
- data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +4 -1
- data/lib/tzinfo/data_sources/zoneinfo_reader.rb +202 -7
- data/lib/tzinfo/time_with_offset.rb +26 -0
- data/lib/tzinfo/transition_rule.rb +455 -0
- data/lib/tzinfo/version.rb +1 -1
- metadata +12 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31e0ea9f4896e07430d20f4411903cad49ceb7bbde32bd156e3646376041c0d7
|
4
|
+
data.tar.gz: c91451850421ca0b8cf3b59b5b318a8e8e9b73ff9b87acf546fa28b907665f82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed6bd3db29c977cb0d92e01551711e6b1285e3db54d4670893dc7716d54ce869ac5115faca62c7c6497eb656af1eecad71a6eedfcc8900eaf324ce2441ba0d01
|
7
|
+
data.tar.gz: 2675aef510ec569141e29c202d5a353590bd52e9fb9aa41489c467c204a6e203b43274f7035acf18d1bafe37765282dbbd0301f4dbf8e94aef01bcc623cb0fd9
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGES.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Changes
|
2
2
|
|
3
|
+
## Version 2.0.3 - 8-Nov-2020
|
4
|
+
|
5
|
+
* Added support for handling "slim" format zoneinfo files that are produced by
|
6
|
+
default by zic version 2020b and later. The POSIX-style TZ string is now used
|
7
|
+
calculate DST transition times after the final defined transition in the file.
|
8
|
+
#120.
|
9
|
+
* Fixed `TimeWithOffset#getlocal` returning a `TimeWithOffset` with the
|
10
|
+
`timezone_offset` still assigned when called with an offset argument on JRuby
|
11
|
+
9.3.
|
12
|
+
* Rubinius is no longer supported.
|
13
|
+
|
14
|
+
|
3
15
|
## Version 2.0.2 - 2-Apr-2020
|
4
16
|
|
5
17
|
* Fixed 'wrong number of arguments' errors when running on JRuby 9.0. #114.
|
@@ -162,6 +174,16 @@
|
|
162
174
|
`TZInfo::Country.get('US').zone_identifiers` should be used instead.
|
163
175
|
|
164
176
|
|
177
|
+
## Version 1.2.8 - 8-Nov-2020
|
178
|
+
|
179
|
+
* Added support for handling "slim" format zoneinfo files that are produced by
|
180
|
+
default by zic version 2020b and later. The POSIX-style TZ string is now used
|
181
|
+
calculate DST transition times after the final defined transition in the file.
|
182
|
+
The 64-bit section is now always used regardless of whether Time has support
|
183
|
+
for 64-bit times. #120.
|
184
|
+
* Rubinius is no longer supported.
|
185
|
+
|
186
|
+
|
165
187
|
## Version 1.2.7 - 2-Apr-2020
|
166
188
|
|
167
189
|
* Fixed 'wrong number of arguments' errors when running on JRuby 9.0. #114.
|
@@ -302,6 +324,30 @@
|
|
302
324
|
use other `TimezonePeriod` instance methods instead (issue #7655).
|
303
325
|
|
304
326
|
|
327
|
+
## Version 0.3.58 (tzdata v2020d) - 8-Nov-2020
|
328
|
+
|
329
|
+
* Updated to tzdata version 2020d
|
330
|
+
(https://mm.icann.org/pipermail/tz-announce/2020-October/000062.html).
|
331
|
+
|
332
|
+
|
333
|
+
## Version 0.3.57 (tzdata v2020a) - 17-May-2020
|
334
|
+
|
335
|
+
* Updated to tzdata version 2020a
|
336
|
+
(<https://mm.icann.org/pipermail/tz-announce/2020-April/000058.html>).
|
337
|
+
|
338
|
+
|
339
|
+
## Version 0.3.56 (tzdata v2019c) - 1-Nov-2019
|
340
|
+
|
341
|
+
* Updated to tzdata version 2019c
|
342
|
+
(<https://mm.icann.org/pipermail/tz-announce/2019-September/000057.html>).
|
343
|
+
|
344
|
+
|
345
|
+
## Version 0.3.55 (tzdata v2018g) - 27-Oct-2018
|
346
|
+
|
347
|
+
* Updated to tzdata version 2018g
|
348
|
+
(<https://mm.icann.org/pipermail/tz-announce/2018-October/000052.html>).
|
349
|
+
|
350
|
+
|
305
351
|
## Version 0.3.54 (tzdata v2018d) - 25-Mar-2018
|
306
352
|
|
307
353
|
* Updated to tzdata version 2018d
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# TZInfo - Ruby Time Zone Library
|
2
2
|
|
3
|
-
[](https://rubygems.org/gems/tzinfo) [](https://travis-ci.
|
3
|
+
[](https://rubygems.org/gems/tzinfo) [](https://travis-ci.com/github/tzinfo/tzinfo) [](https://ci.appveyor.com/project/philr/tzinfo)
|
4
4
|
|
5
5
|
[TZInfo](https://tzinfo.github.io) is a Ruby library that provides access to
|
6
6
|
time zone data and allows times to be converted using time zone rules.
|
@@ -368,8 +368,8 @@ claims.
|
|
368
368
|
|
369
369
|
## Compatibility
|
370
370
|
|
371
|
-
TZInfo v2.0.0 requires a minimum of Ruby MRI 1.9.3
|
372
|
-
later)
|
371
|
+
TZInfo v2.0.0 requires a minimum of Ruby MRI 1.9.3 or JRuby 1.7 (in 1.9 mode or
|
372
|
+
later).
|
373
373
|
|
374
374
|
|
375
375
|
## Thread-Safety
|
data/lib/tzinfo.rb
CHANGED
@@ -25,6 +25,8 @@ require_relative 'tzinfo/timestamp_with_offset'
|
|
25
25
|
|
26
26
|
require_relative 'tzinfo/timezone_offset'
|
27
27
|
require_relative 'tzinfo/timezone_transition'
|
28
|
+
require_relative 'tzinfo/transition_rule'
|
29
|
+
require_relative 'tzinfo/annual_rules'
|
28
30
|
|
29
31
|
require_relative 'tzinfo/data_sources'
|
30
32
|
require_relative 'tzinfo/data_sources/timezone_info'
|
@@ -35,6 +37,7 @@ require_relative 'tzinfo/data_sources/transitions_data_timezone_info'
|
|
35
37
|
|
36
38
|
require_relative 'tzinfo/data_sources/country_info'
|
37
39
|
|
40
|
+
require_relative 'tzinfo/data_sources/posix_time_zone_parser'
|
38
41
|
require_relative 'tzinfo/data_sources/zoneinfo_reader'
|
39
42
|
|
40
43
|
require_relative 'tzinfo/data_source'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TZInfo
|
5
|
+
# A set of rules that define when transitions occur in time zones with
|
6
|
+
# annually occurring daylight savings time.
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class AnnualRules #:nodoc:
|
10
|
+
# @return [TimezoneOffset] the standard offset that applies when daylight
|
11
|
+
# savings time is not in force.
|
12
|
+
attr_reader :std_offset
|
13
|
+
|
14
|
+
# @return [TimezoneOffset] the offset that applies when daylight savings
|
15
|
+
# time is in force.
|
16
|
+
attr_reader :dst_offset
|
17
|
+
|
18
|
+
# @return [TransitionRule] the rule that determines when daylight savings
|
19
|
+
# time starts.
|
20
|
+
attr_reader :dst_start_rule
|
21
|
+
|
22
|
+
# @return [TransitionRule] the rule that determines when daylight savings
|
23
|
+
# time ends.
|
24
|
+
attr_reader :dst_end_rule
|
25
|
+
|
26
|
+
# Initializes a new {AnnualRules} instance.
|
27
|
+
#
|
28
|
+
# @param std_offset [TimezoneOffset] the standard offset that applies when
|
29
|
+
# daylight savings time is not in force.
|
30
|
+
# @param dst_offset [TimezoneOffset] the offset that applies when daylight
|
31
|
+
# savings time is in force.
|
32
|
+
# @param dst_start_rule [TransitionRule] the rule that determines when
|
33
|
+
# daylight savings time starts.
|
34
|
+
# @param dst_end_rule [TransitionRule] the rule that determines when daylight
|
35
|
+
# savings time ends.
|
36
|
+
def initialize(std_offset, dst_offset, dst_start_rule, dst_end_rule)
|
37
|
+
@std_offset = std_offset
|
38
|
+
@dst_offset = dst_offset
|
39
|
+
@dst_start_rule = dst_start_rule
|
40
|
+
@dst_end_rule = dst_end_rule
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the transitions between standard and daylight savings time for a
|
44
|
+
# given year. The results are ordered by time of occurrence (earliest to
|
45
|
+
# latest).
|
46
|
+
#
|
47
|
+
# @param year [Integer] the year to calculate transitions for.
|
48
|
+
# @return [Array<TimezoneTransition>] the transitions for the year.
|
49
|
+
def transitions(year)
|
50
|
+
start_dst = apply_rule(@dst_start_rule, @std_offset, @dst_offset, year)
|
51
|
+
end_dst = apply_rule(@dst_end_rule, @dst_offset, @std_offset, year)
|
52
|
+
|
53
|
+
end_dst.timestamp_value < start_dst.timestamp_value ? [end_dst, start_dst] : [start_dst, end_dst]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Applies a given rule between offsets on a year.
|
59
|
+
#
|
60
|
+
# @param rule [TransitionRule] the rule to apply.
|
61
|
+
# @param from_offset [TimezoneOffset] the offset the rule transitions from.
|
62
|
+
# @param to_offset [TimezoneOffset] the offset the rule transitions to.
|
63
|
+
# @param year [Integer] the year when the transition occurs.
|
64
|
+
# @return [TimezoneTransition] the transition determined by the rule.
|
65
|
+
def apply_rule(rule, from_offset, to_offset, year)
|
66
|
+
at = rule.at(from_offset, year)
|
67
|
+
TimezoneTransition.new(to_offset, from_offset, at.value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
private_constant :AnnualRules
|
71
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
module TZInfo
|
7
|
+
# Use send as a workaround for erroneous 'wrong number of arguments' errors
|
8
|
+
# with JRuby 9.0.5.0 when calling methods with Java implementations. See #114.
|
9
|
+
send(:using, UntaintExt) if TZInfo.const_defined?(:UntaintExt)
|
10
|
+
|
11
|
+
module DataSources
|
12
|
+
# An {InvalidPosixTimeZone} exception is raised if an invalid POSIX-style
|
13
|
+
# time zone string is encountered.
|
14
|
+
#
|
15
|
+
# @private
|
16
|
+
class InvalidPosixTimeZone < StandardError #:nodoc:
|
17
|
+
end
|
18
|
+
private_constant :InvalidPosixTimeZone
|
19
|
+
|
20
|
+
# A parser for POSIX-style TZ strings used in zoneinfo files and specified
|
21
|
+
# by tzfile.5 and tzset.3.
|
22
|
+
#
|
23
|
+
# @private
|
24
|
+
class PosixTimeZoneParser #:nodoc:
|
25
|
+
# Initializes a new {PosixTimeZoneParser}.
|
26
|
+
#
|
27
|
+
# @param string_deduper [StringDeduper] a {StringDeduper} instance to use
|
28
|
+
# to dedupe abbreviations.
|
29
|
+
def initialize(string_deduper)
|
30
|
+
@string_deduper = string_deduper
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parses a POSIX-style TZ string.
|
34
|
+
#
|
35
|
+
# @param tz_string [String] the string to parse.
|
36
|
+
# @return [Object] either a {TimezoneOffset} for a constantly applied
|
37
|
+
# offset or an {AnnualRules} instance representing the rules.
|
38
|
+
# @raise [InvalidPosixTimeZone] if `tz_string` is not a `String`.
|
39
|
+
# @raise [InvalidPosixTimeZone] if `tz_string` is is not valid.
|
40
|
+
def parse(tz_string)
|
41
|
+
raise InvalidPosixTimeZone unless tz_string.kind_of?(String)
|
42
|
+
return nil if tz_string.empty?
|
43
|
+
|
44
|
+
s = StringScanner.new(tz_string)
|
45
|
+
check_scan(s, /([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
|
46
|
+
std_abbrev = @string_deduper.dedupe((s[1] || s[2]).untaint)
|
47
|
+
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
|
48
|
+
std_offset = get_offset_from_hms(s[1], s[2], s[3])
|
49
|
+
|
50
|
+
if s.scan(/([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
|
51
|
+
dst_abbrev = @string_deduper.dedupe((s[1] || s[2]).untaint)
|
52
|
+
|
53
|
+
if s.scan(/([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
|
54
|
+
dst_offset = get_offset_from_hms(s[1], s[2], s[3])
|
55
|
+
else
|
56
|
+
# POSIX is negative for ahead of UTC.
|
57
|
+
dst_offset = std_offset - 3600
|
58
|
+
end
|
59
|
+
|
60
|
+
dst_difference = std_offset - dst_offset
|
61
|
+
|
62
|
+
start_rule = parse_rule(s, 'start')
|
63
|
+
end_rule = parse_rule(s, 'end')
|
64
|
+
|
65
|
+
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." if s.rest?
|
66
|
+
|
67
|
+
if start_rule.is_always_first_day_of_year? && start_rule.transition_at == 0 &&
|
68
|
+
end_rule.is_always_last_day_of_year? && end_rule.transition_at == 86400 + dst_difference
|
69
|
+
# Constant daylight savings time.
|
70
|
+
# POSIX is negative for ahead of UTC.
|
71
|
+
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev)
|
72
|
+
else
|
73
|
+
AnnualRules.new(
|
74
|
+
TimezoneOffset.new(-std_offset, 0, std_abbrev),
|
75
|
+
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev),
|
76
|
+
start_rule,
|
77
|
+
end_rule)
|
78
|
+
end
|
79
|
+
elsif !s.rest?
|
80
|
+
# Constant standard time.
|
81
|
+
# POSIX is negative for ahead of UTC.
|
82
|
+
TimezoneOffset.new(-std_offset, 0, std_abbrev)
|
83
|
+
else
|
84
|
+
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Parses a rule.
|
91
|
+
#
|
92
|
+
# @param s [StringScanner] the `StringScanner` to read the rule from.
|
93
|
+
# @param type [String] the type of rule (either `'start'` or `'end'`).
|
94
|
+
# @raise [InvalidPosixTimeZone] if the rule is not valid.
|
95
|
+
# @return [TransitionRule] the parsed rule.
|
96
|
+
def parse_rule(s, type)
|
97
|
+
check_scan(s, /,(?: (?: J(\d+) ) | (\d+) | (?: M(\d+)\.(\d)\.(\d) ) )/x)
|
98
|
+
julian_day_of_year = s[1]
|
99
|
+
absolute_day_of_year = s[2]
|
100
|
+
month = s[3]
|
101
|
+
week = s[4]
|
102
|
+
day_of_week = s[5]
|
103
|
+
|
104
|
+
if s.scan(/\//)
|
105
|
+
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
|
106
|
+
transition_at = get_seconds_after_midnight_from_hms(s[1], s[2], s[3])
|
107
|
+
else
|
108
|
+
transition_at = 7200
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
if julian_day_of_year
|
113
|
+
JulianDayOfYearTransitionRule.new(julian_day_of_year.to_i, transition_at)
|
114
|
+
elsif absolute_day_of_year
|
115
|
+
AbsoluteDayOfYearTransitionRule.new(absolute_day_of_year.to_i, transition_at)
|
116
|
+
elsif week == '5'
|
117
|
+
LastDayOfMonthTransitionRule.new(month.to_i, day_of_week.to_i, transition_at)
|
118
|
+
else
|
119
|
+
DayOfMonthTransitionRule.new(month.to_i, week.to_i, day_of_week.to_i, transition_at)
|
120
|
+
end
|
121
|
+
rescue ArgumentError => e
|
122
|
+
raise InvalidPosixTimeZone, "Invalid #{type} rule in POSIX-style time zone string: #{e}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns an offset in seconds from hh:mm:ss values. The value can be
|
127
|
+
# negative. -02:33:12 would represent 2 hours, 33 minutes and 12 seconds
|
128
|
+
# ahead of UTC.
|
129
|
+
#
|
130
|
+
# @param h [String] the hours.
|
131
|
+
# @param m [String] the minutes.
|
132
|
+
# @param s [String] the seconds.
|
133
|
+
# @return [Integer] the offset.
|
134
|
+
# @raise [InvalidPosixTimeZone] if the mm and ss values are greater than
|
135
|
+
# 59.
|
136
|
+
def get_offset_from_hms(h, m, s)
|
137
|
+
h = h.to_i
|
138
|
+
m = m.to_i
|
139
|
+
s = s.to_i
|
140
|
+
raise InvalidPosixTimeZone, "Invalid minute #{m} in offset for POSIX-style time zone string." if m > 59
|
141
|
+
raise InvalidPosixTimeZone, "Invalid second #{s} in offset for POSIX-style time zone string." if s > 59
|
142
|
+
magnitude = (h.abs * 60 + m) * 60 + s
|
143
|
+
h < 0 ? -magnitude : magnitude
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the seconds from midnight from hh:mm:ss values. Hours can exceed
|
147
|
+
# 24 for a time on the following day. Hours can be negative to subtract
|
148
|
+
# hours from midnight on the given day. -02:33:12 represents 22:33:12 on
|
149
|
+
# the prior day.
|
150
|
+
#
|
151
|
+
# @param h [String] the hour.
|
152
|
+
# @param m [String] the minutes past the hour.
|
153
|
+
# @param s [String] the seconds past the minute.
|
154
|
+
# @return [Integer] the number of seconds after midnight.
|
155
|
+
# @raise [InvalidPosixTimeZone] if the mm and ss values are greater than
|
156
|
+
# 59.
|
157
|
+
def get_seconds_after_midnight_from_hms(h, m, s)
|
158
|
+
h = h.to_i
|
159
|
+
m = m.to_i
|
160
|
+
s = s.to_i
|
161
|
+
raise InvalidPosixTimeZone, "Invalid minute #{m} in time for POSIX-style time zone string." if m > 59
|
162
|
+
raise InvalidPosixTimeZone, "Invalid second #{s} in time for POSIX-style time zone string." if s > 59
|
163
|
+
(h * 3600) + m * 60 + s
|
164
|
+
end
|
165
|
+
|
166
|
+
# Scans for a pattern and raises an exception if the pattern does not
|
167
|
+
# match the input.
|
168
|
+
#
|
169
|
+
# @param s [StringScanner] the `StringScanner` to scan.
|
170
|
+
# @param pattern [Regexp] the pattern to match.
|
171
|
+
# @return [String] the result of the scan.
|
172
|
+
# @raise [InvalidPosixTimeZone] if the pattern does not match the input.
|
173
|
+
def check_scan(s, pattern)
|
174
|
+
result = s.scan(pattern)
|
175
|
+
raise InvalidPosixTimeZone, "Expected '#{s.rest}' to match #{pattern} in POSIX-style time zone string." unless result
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
private_constant :PosixTimeZoneParser
|
180
|
+
end
|
181
|
+
end
|
@@ -237,7 +237,10 @@ module TZInfo
|
|
237
237
|
@timezone_identifiers = load_timezone_identifiers.freeze
|
238
238
|
@countries = load_countries(iso3166_tab_path, zone_tab_path).freeze
|
239
239
|
@country_codes = @countries.keys.sort!.freeze
|
240
|
-
|
240
|
+
|
241
|
+
string_deduper = ConcurrentStringDeduper.new
|
242
|
+
posix_tz_parser = PosixTimeZoneParser.new(string_deduper)
|
243
|
+
@zoneinfo_reader = ZoneinfoReader.new(posix_tz_parser, string_deduper)
|
241
244
|
end
|
242
245
|
|
243
246
|
# Returns a frozen `Array` of all the available time zone identifiers. The
|
@@ -14,11 +14,20 @@ module TZInfo
|
|
14
14
|
|
15
15
|
# Reads compiled zoneinfo TZif (\0, 2 or 3) files.
|
16
16
|
class ZoneinfoReader #:nodoc:
|
17
|
+
# The year to generate transitions up to.
|
18
|
+
#
|
19
|
+
# @private
|
20
|
+
GENERATE_UP_TO = Time.now.utc.year + 100
|
21
|
+
private_constant :GENERATE_UP_TO
|
22
|
+
|
17
23
|
# Initializes a new {ZoneinfoReader}.
|
18
24
|
#
|
25
|
+
# @param posix_tz_parser [PosixTimeZoneParser] a {PosixTimeZoneParser}
|
26
|
+
# instance to use to parse POSIX-style TZ strings.
|
19
27
|
# @param string_deduper [StringDeduper] a {StringDeduper} instance to use
|
20
|
-
#
|
21
|
-
def initialize(string_deduper)
|
28
|
+
# to dedupe abbreviations.
|
29
|
+
def initialize(posix_tz_parser, string_deduper)
|
30
|
+
@posix_tz_parser = posix_tz_parser
|
22
31
|
@string_deduper = string_deduper
|
23
32
|
end
|
24
33
|
|
@@ -163,6 +172,168 @@ module TZInfo
|
|
163
172
|
first_offset_index
|
164
173
|
end
|
165
174
|
|
175
|
+
# Determines if the offset from a transition matches the offset from a
|
176
|
+
# rule. This is a looser match than equality, not requiring that the
|
177
|
+
# base_utc_offset and std_offset both match (which have to be derived for
|
178
|
+
# transitions, but are known for rules.
|
179
|
+
#
|
180
|
+
# @param offset [TimezoneOffset] an offset from a transition.
|
181
|
+
# @param rule_offset [TimezoneOffset] an offset from a rule.
|
182
|
+
# @return [Boolean] whether the offsets match.
|
183
|
+
def offset_matches_rule?(offset, rule_offset)
|
184
|
+
offset.observed_utc_offset == rule_offset.observed_utc_offset &&
|
185
|
+
offset.dst? == rule_offset.dst? &&
|
186
|
+
offset.abbreviation == rule_offset.abbreviation
|
187
|
+
end
|
188
|
+
|
189
|
+
# Apply the rules from the TZ string when there were no defined
|
190
|
+
# transitions. Checks for a matching offset. Returns the rules-based
|
191
|
+
# constant offset or generates transitions from 1970 until 100 years into
|
192
|
+
# the future (at the time of loading zoneinfo_reader.rb).
|
193
|
+
#
|
194
|
+
# @param file [IO] the file being processed.
|
195
|
+
# @param first_offset [TimezoneOffset] the first offset included in the
|
196
|
+
# file that would normally apply without the rules.
|
197
|
+
# @param rules [Object] a {TimezoneOffset} specifying a constant offset or
|
198
|
+
# {AnnualRules} instance specfying transitions.
|
199
|
+
# @return [Object] either a {TimezoneOffset} or an `Array` of
|
200
|
+
# {TimezoneTransition}s.
|
201
|
+
# @raise [InvalidZoneinfoFile] if the first offset does not match the
|
202
|
+
# rules.
|
203
|
+
def apply_rules_without_transitions(file, first_offset, rules)
|
204
|
+
if rules.kind_of?(TimezoneOffset)
|
205
|
+
unless offset_matches_rule?(first_offset, rules)
|
206
|
+
raise InvalidZoneinfoFile, "Constant offset POSIX-style TZ string does not match constant offset in file '#{file.path}'."
|
207
|
+
end
|
208
|
+
rules
|
209
|
+
else
|
210
|
+
transitions = 1970.upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
|
211
|
+
first_transition = transitions[0]
|
212
|
+
|
213
|
+
unless offset_matches_rule?(first_offset, first_transition.previous_offset)
|
214
|
+
# Not transitioning from the designated first offset.
|
215
|
+
|
216
|
+
if offset_matches_rule?(first_offset, first_transition.offset)
|
217
|
+
# Skip an unnecessary transition to the first offset.
|
218
|
+
transitions.shift
|
219
|
+
else
|
220
|
+
# The initial offset doesn't match the ongoing rules. Replace the
|
221
|
+
# previous offset of the first transition.
|
222
|
+
transitions[0] = TimezoneTransition.new(first_transition.offset, first_offset, first_transition.timestamp_value)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
transitions
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Finds an offset that is equivalent to the one specified in the given
|
231
|
+
# `Array`. Matching is performed with {TimezoneOffset#==}.
|
232
|
+
#
|
233
|
+
# @param offsets [Array<TimezoneOffset>] an `Array` to search.
|
234
|
+
# @param offset [TimezoneOffset] the offset to search for.
|
235
|
+
# @return [TimezoneOffset] the matching offset from `offsets` or `nil`
|
236
|
+
# if not found.
|
237
|
+
def find_existing_offset(offsets, offset)
|
238
|
+
offsets.find {|o| o == offset }
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns a new AnnualRules instance with standard and daylight savings
|
242
|
+
# offsets replaced with equivalents from an array. This reduces the memory
|
243
|
+
# requirement for loaded time zones by reusing offsets for rule-generated
|
244
|
+
# transitions.
|
245
|
+
#
|
246
|
+
# @param offsets [Array<TimezoneOffset>] an `Array` to search for
|
247
|
+
# equivalent offsets.
|
248
|
+
# @param annual_rules [AnnualRules] the {AnnualRules} instance to check.
|
249
|
+
# @return [AnnualRules] either a new {AnnualRules} instance with either
|
250
|
+
# the {AnnualRules#std_offset std_offset} or {AnnualRules#dst_offset
|
251
|
+
# dst_offset} replaced, or the original instance if no equivalent for
|
252
|
+
# either {AnnualRules#std_offset std_offset} or {AnnualRules#dst_offset
|
253
|
+
# dst_offset} could be found.
|
254
|
+
def replace_with_existing_offsets(offsets, annual_rules)
|
255
|
+
existing_std_offset = find_existing_offset(offsets, annual_rules.std_offset)
|
256
|
+
existing_dst_offset = find_existing_offset(offsets, annual_rules.dst_offset)
|
257
|
+
if existing_std_offset || existing_dst_offset
|
258
|
+
AnnualRules.new(existing_std_offset || annual_rules.std_offset, existing_dst_offset || annual_rules.dst_offset,
|
259
|
+
annual_rules.dst_start_rule, annual_rules.dst_end_rule)
|
260
|
+
else
|
261
|
+
annual_rules
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Validates the offset indicated to be observed by the rules before the
|
266
|
+
# first generated transition against the offset of the last defined
|
267
|
+
# transition.
|
268
|
+
#
|
269
|
+
# Fix the last defined transition if it differ on just base/std offsets
|
270
|
+
# (which are derived). Raise an error if the observed UTC offset or
|
271
|
+
# abbreviations differ.
|
272
|
+
#
|
273
|
+
# @param file [IO] the file being processed.
|
274
|
+
# @param last_defined [TimezoneTransition] the last defined transition in
|
275
|
+
# the file.
|
276
|
+
# @param first_rule_offset [TimezoneOffset] the offset the rules indicate
|
277
|
+
# is observed prior to the first rules generated transition.
|
278
|
+
# @return [TimezoneTransition] the last defined transition (either the
|
279
|
+
# original instance or a replacement).
|
280
|
+
# @raise [InvalidZoneinfoFile] if the offset of {last_defined} and
|
281
|
+
# {first_rule_offset} do not match.
|
282
|
+
def validate_and_fix_last_defined_transition_offset(file, last_defined, first_rule_offset)
|
283
|
+
offset_of_last_defined = last_defined.offset
|
284
|
+
|
285
|
+
if offset_of_last_defined == first_rule_offset
|
286
|
+
last_defined
|
287
|
+
else
|
288
|
+
if offset_matches_rule?(offset_of_last_defined, first_rule_offset)
|
289
|
+
# The same overall offset, but differing in the base or std
|
290
|
+
# offset (which are derived). Correct by using the rule.
|
291
|
+
TimezoneTransition.new(first_rule_offset, last_defined.previous_offset, last_defined.timestamp_value)
|
292
|
+
else
|
293
|
+
raise InvalidZoneinfoFile, "The first offset indicated by the POSIX-style TZ string did not match the final defined offset in file '#{file.path}'."
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Apply the rules from the TZ string when there were defined
|
299
|
+
# transitions. Checks for a matching offset with the last transition.
|
300
|
+
# Redefines the last transition if required and if the rules don't
|
301
|
+
# specific a constant offset, generates transitions until 100 years into
|
302
|
+
# the future (at the time of loading zoneinfo_reader.rb).
|
303
|
+
#
|
304
|
+
# @param file [IO] the file being processed.
|
305
|
+
# @param transitions [Array<TimezoneTransition>] the defined transitions.
|
306
|
+
# @param offsets [Array<TimezoneOffset>] the offsets used by the defined
|
307
|
+
# transitions.
|
308
|
+
# @param rules [Object] a {TimezoneOffset} specifying a constant offset or
|
309
|
+
# {AnnualRules} instance specfying transitions.
|
310
|
+
# @raise [InvalidZoneinfoFile] if the first offset does not match the
|
311
|
+
# rules.
|
312
|
+
# @raise [InvalidZoneinfoFile] if the previous offset of the first
|
313
|
+
# generated transition does not match the offset of the last defined
|
314
|
+
# transition.
|
315
|
+
def apply_rules_with_transitions(file, transitions, offsets, rules)
|
316
|
+
last_defined = transitions[-1]
|
317
|
+
|
318
|
+
if rules.kind_of?(TimezoneOffset)
|
319
|
+
transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, rules)
|
320
|
+
else
|
321
|
+
last_year = last_defined.local_end_at.to_time.year
|
322
|
+
|
323
|
+
if last_year <= GENERATE_UP_TO
|
324
|
+
rules = replace_with_existing_offsets(offsets, rules)
|
325
|
+
|
326
|
+
generated = rules.transitions(last_year).find_all {|t| t.timestamp_value > last_defined.timestamp_value } +
|
327
|
+
(last_year + 1).upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
|
328
|
+
|
329
|
+
unless generated.empty?
|
330
|
+
transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, generated[0].previous_offset)
|
331
|
+
transitions.concat(generated)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
166
337
|
# Parses a zoneinfo file and returns either a {TimezoneOffset} that is
|
167
338
|
# constantly observed or an `Array` of {TimezoneTransition}s.
|
168
339
|
#
|
@@ -171,7 +342,7 @@ module TZInfo
|
|
171
342
|
# {TimezoneTransition}s.
|
172
343
|
# @raise [InvalidZoneinfoFile] if the file is not a valid zoneinfo file.
|
173
344
|
def parse(file)
|
174
|
-
magic, version,
|
345
|
+
magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
|
175
346
|
check_read(file, 44).unpack('a4 a x15 NNNNNN')
|
176
347
|
|
177
348
|
if magic != 'TZif'
|
@@ -180,11 +351,11 @@ module TZInfo
|
|
180
351
|
|
181
352
|
if version == '2' || version == '3'
|
182
353
|
# Skip the first 32-bit section and read the header of the second 64-bit section
|
183
|
-
file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 +
|
354
|
+
file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
|
184
355
|
|
185
356
|
prev_version = version
|
186
357
|
|
187
|
-
magic, version,
|
358
|
+
magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
|
188
359
|
check_read(file, 44).unpack('a4 a x15 NNNNNN')
|
189
360
|
|
190
361
|
unless magic == 'TZif' && (version == prev_version)
|
@@ -229,6 +400,23 @@ module TZInfo
|
|
229
400
|
|
230
401
|
abbrev = check_read(file, charcnt)
|
231
402
|
|
403
|
+
if using_64bit
|
404
|
+
# Skip to the POSIX-style TZ string.
|
405
|
+
file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
|
406
|
+
tz_string_start = check_read(file, 1)
|
407
|
+
raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
|
408
|
+
tz_string = file.readline("\n").force_encoding(Encoding::UTF_8)
|
409
|
+
raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
|
410
|
+
|
411
|
+
begin
|
412
|
+
rules = @posix_tz_parser.parse(tz_string)
|
413
|
+
rescue InvalidPosixTimeZone => e
|
414
|
+
raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
|
415
|
+
end
|
416
|
+
else
|
417
|
+
rules = nil
|
418
|
+
end
|
419
|
+
|
232
420
|
# Derive the offsets from standard time (std_offset).
|
233
421
|
first_offset_index = derive_offsets(transitions, offsets)
|
234
422
|
|
@@ -266,12 +454,16 @@ module TZInfo
|
|
266
454
|
|
267
455
|
|
268
456
|
if transitions.empty?
|
269
|
-
|
457
|
+
if rules
|
458
|
+
apply_rules_without_transitions(file, first_offset, rules)
|
459
|
+
else
|
460
|
+
first_offset
|
461
|
+
end
|
270
462
|
else
|
271
463
|
previous_offset = first_offset
|
272
464
|
previous_at = nil
|
273
465
|
|
274
|
-
transitions.map do |t|
|
466
|
+
transitions = transitions.map do |t|
|
275
467
|
offset = offsets[t[:offset]]
|
276
468
|
at = t[:at]
|
277
469
|
raise InvalidZoneinfoFile, "Transition at #{at} is not later than the previous transition at #{previous_at} in file '#{file.path}'." if previous_at && previous_at >= at
|
@@ -280,6 +472,9 @@ module TZInfo
|
|
280
472
|
previous_at = at
|
281
473
|
tt
|
282
474
|
end
|
475
|
+
|
476
|
+
apply_rules_with_transitions(file, transitions, offsets, rules) if rules
|
477
|
+
transitions
|
283
478
|
end
|
284
479
|
end
|
285
480
|
end
|
@@ -46,6 +46,22 @@ module TZInfo
|
|
46
46
|
end
|
47
47
|
alias isdst dst?
|
48
48
|
|
49
|
+
# An overridden version of `Time#getlocal` that clears the associated
|
50
|
+
# {TimezoneOffset} if the base implementation of `getlocal` returns a
|
51
|
+
# {TimeWithOffset}.
|
52
|
+
#
|
53
|
+
# @return [Time] a representation of the {TimeWithOffset} using either the
|
54
|
+
# local time zone or the given offset.
|
55
|
+
def getlocal(*args)
|
56
|
+
# JRuby < 9.3 returns a Time in all cases.
|
57
|
+
# JRuby >= 9.3 returns a Time when called with no arguments and a
|
58
|
+
# TimeWithOffset with a timezone_offset assigned when called with an
|
59
|
+
# offset argument.
|
60
|
+
result = super
|
61
|
+
result.clear_timezone_offset if result.kind_of?(TimeWithOffset)
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
49
65
|
# An overridden version of `Time#gmtime` that clears the associated
|
50
66
|
# {TimezoneOffset}.
|
51
67
|
#
|
@@ -124,5 +140,15 @@ module TZInfo
|
|
124
140
|
result.set_timezone_offset(o)
|
125
141
|
end
|
126
142
|
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
# Clears the associated {TimezoneOffset}.
|
147
|
+
#
|
148
|
+
# @return [TimeWithOffset] `self`.
|
149
|
+
def clear_timezone_offset
|
150
|
+
@timezone_offset = nil
|
151
|
+
self
|
152
|
+
end
|
127
153
|
end
|
128
154
|
end
|
@@ -0,0 +1,455 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TZInfo
|
5
|
+
# Base class for rules definining the transition between standard and daylight
|
6
|
+
# savings time.
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
# @private
|
10
|
+
class TransitionRule #:nodoc:
|
11
|
+
# Returns the number of seconds after midnight local time on the day
|
12
|
+
# identified by the rule at which the transition occurs. Can be negative to
|
13
|
+
# denote a time on the prior day. Can be greater than or equal to 86,400 to
|
14
|
+
# denote a time of the following day.
|
15
|
+
#
|
16
|
+
# @return [Integer] the time in seconds after midnight local time at which
|
17
|
+
# the transition occurs.
|
18
|
+
attr_reader :transition_at
|
19
|
+
|
20
|
+
# Initializes a new {TransitionRule}.
|
21
|
+
#
|
22
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
23
|
+
# time at which the transition occurs.
|
24
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
25
|
+
def initialize(transition_at)
|
26
|
+
raise ArgumentError, 'Invalid transition_at' unless transition_at.kind_of?(Integer)
|
27
|
+
@transition_at = transition_at
|
28
|
+
end
|
29
|
+
|
30
|
+
# Calculates the time of the transition from a given offset on a given year.
|
31
|
+
#
|
32
|
+
# @param offset [TimezoneOffset] the current offset at the time the rule
|
33
|
+
# will transition.
|
34
|
+
# @param year [Integer] the year in which the transition occurs (local
|
35
|
+
# time).
|
36
|
+
# @return [TimestampWithOffset] the time at which the transition occurs.
|
37
|
+
def at(offset, year)
|
38
|
+
day = get_day(offset, year)
|
39
|
+
TimestampWithOffset.set_timezone_offset(Timestamp.for(day + @transition_at), offset)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Determines if this {TransitionRule} is equal to another instance.
|
43
|
+
#
|
44
|
+
# @param r [Object] the instance to test for equality.
|
45
|
+
# @return [Boolean] `true` if `r` is a {TransitionRule} with the same
|
46
|
+
# {transition_at} as this {TransitionRule}, otherwise `false`.
|
47
|
+
def ==(r)
|
48
|
+
r.kind_of?(TransitionRule) && @transition_at == r.transition_at
|
49
|
+
end
|
50
|
+
alias eql? ==
|
51
|
+
|
52
|
+
# @return [Integer] a hash based on {hash_args} (defaulting to
|
53
|
+
# {transition_at}).
|
54
|
+
def hash
|
55
|
+
hash_args.hash
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# @return [Array] an `Array` of parameters that will influence the output of
|
61
|
+
# {hash}.
|
62
|
+
def hash_args
|
63
|
+
[@transition_at]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
private_constant :TransitionRule
|
67
|
+
|
68
|
+
# A base class for transition rules that activate based on an integer day of
|
69
|
+
# the year.
|
70
|
+
#
|
71
|
+
# @abstract
|
72
|
+
# @private
|
73
|
+
class DayOfYearTransitionRule < TransitionRule #:nodoc:
|
74
|
+
# Initializes a new {DayOfYearTransitionRule}.
|
75
|
+
#
|
76
|
+
# @param day [Integer] the day of the year on which the transition occurs.
|
77
|
+
# The precise meaning is defined by subclasses.
|
78
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
79
|
+
# time at which the transition occurs.
|
80
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
81
|
+
# @raise [ArgumentError] if `day` is not an `Integer`.
|
82
|
+
def initialize(day, transition_at)
|
83
|
+
super(transition_at)
|
84
|
+
raise ArgumentError, 'Invalid day' unless day.kind_of?(Integer)
|
85
|
+
@seconds = day * 86400
|
86
|
+
end
|
87
|
+
|
88
|
+
# Determines if this {DayOfYearTransitionRule} is equal to another instance.
|
89
|
+
#
|
90
|
+
# @param r [Object] the instance to test for equality.
|
91
|
+
# @return [Boolean] `true` if `r` is a {DayOfYearTransitionRule} with the
|
92
|
+
# same {transition_at} and day as this {DayOfYearTransitionRule},
|
93
|
+
# otherwise `false`.
|
94
|
+
def ==(r)
|
95
|
+
super(r) && r.kind_of?(DayOfYearTransitionRule) && @seconds == r.seconds
|
96
|
+
end
|
97
|
+
alias eql? ==
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
# @return [Integer] the day multipled by the number of seconds in a day.
|
102
|
+
attr_reader :seconds
|
103
|
+
|
104
|
+
# (see TransitionRule#hash_args)
|
105
|
+
def hash_args
|
106
|
+
[@seconds] + super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
private_constant :DayOfYearTransitionRule
|
110
|
+
|
111
|
+
# Defines transitions that occur on the zero-based nth day of the year.
|
112
|
+
#
|
113
|
+
# Day 0 is 1 January.
|
114
|
+
#
|
115
|
+
# Leap days are counted. Day 59 will be 29 February on a leap year and 1 March
|
116
|
+
# on a non-leap year. Day 365 will be 31 December on a leap year and 1 January
|
117
|
+
# the following year on a non-leap year.
|
118
|
+
#
|
119
|
+
# @private
|
120
|
+
class AbsoluteDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
|
121
|
+
# Initializes a new {AbsoluteDayOfYearTransitionRule}.
|
122
|
+
#
|
123
|
+
# @param day [Integer] the zero-based day of the year on which the
|
124
|
+
# transition occurs (0 to 365 inclusive).
|
125
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
126
|
+
# time at which the transition occurs.
|
127
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
128
|
+
# @raise [ArgumentError] if `day` is not an `Integer`.
|
129
|
+
# @raise [ArgumentError] if `day` is less than 0 or greater than 365.
|
130
|
+
def initialize(day, transition_at = 0)
|
131
|
+
super(day, transition_at)
|
132
|
+
raise ArgumentError, 'Invalid day' unless day >= 0 && day <= 365
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [Boolean] `true` if the day specified by this transition is the
|
136
|
+
# first in the year (a day number of 0), otherwise `false`.
|
137
|
+
def is_always_first_day_of_year?
|
138
|
+
seconds == 0
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Boolean] `false`.
|
142
|
+
def is_always_last_day_of_year?
|
143
|
+
false
|
144
|
+
end
|
145
|
+
|
146
|
+
# Determines if this {AbsoluteDayOfYearTransitionRule} is equal to another
|
147
|
+
# instance.
|
148
|
+
#
|
149
|
+
# @param r [Object] the instance to test for equality.
|
150
|
+
# @return [Boolean] `true` if `r` is a {AbsoluteDayOfYearTransitionRule}
|
151
|
+
# with the same {transition_at} and day as this
|
152
|
+
# {AbsoluteDayOfYearTransitionRule}, otherwise `false`.
|
153
|
+
def ==(r)
|
154
|
+
super(r) && r.kind_of?(AbsoluteDayOfYearTransitionRule)
|
155
|
+
end
|
156
|
+
alias eql? ==
|
157
|
+
|
158
|
+
protected
|
159
|
+
|
160
|
+
# Returns a `Time` representing midnight local time on the day specified by
|
161
|
+
# the rule for the given offset and year.
|
162
|
+
#
|
163
|
+
# @param offset [TimezoneOffset] the current offset at the time of the
|
164
|
+
# transition.
|
165
|
+
# @param year [Integer] the year in which the transition occurs.
|
166
|
+
# @return [Time] midnight local time on the day specified by the rule for
|
167
|
+
# the given offset and year.
|
168
|
+
def get_day(offset, year)
|
169
|
+
Time.new(year, 1, 1, 0, 0, 0, offset.observed_utc_offset) + seconds
|
170
|
+
end
|
171
|
+
|
172
|
+
# (see TransitionRule#hash_args)
|
173
|
+
def hash_args
|
174
|
+
[AbsoluteDayOfYearTransitionRule] + super
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Defines transitions that occur on the one-based nth Julian day of the year.
|
179
|
+
#
|
180
|
+
# Leap days are not counted. Day 1 is 1 January. Day 60 is always 1 March.
|
181
|
+
# Day 365 is always 31 December.
|
182
|
+
#
|
183
|
+
# @private
|
184
|
+
class JulianDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
|
185
|
+
# The 60 days in seconds.
|
186
|
+
LEAP = 60 * 86400
|
187
|
+
private_constant :LEAP
|
188
|
+
|
189
|
+
# The length of a non-leap year in seconds.
|
190
|
+
YEAR = 365 * 86400
|
191
|
+
private_constant :YEAR
|
192
|
+
|
193
|
+
# Initializes a new {JulianDayOfYearTransitionRule}.
|
194
|
+
#
|
195
|
+
# @param day [Integer] the one-based Julian day of the year on which the
|
196
|
+
# transition occurs (1 to 365 inclusive).
|
197
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
198
|
+
# time at which the transition occurs.
|
199
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
200
|
+
# @raise [ArgumentError] if `day` is not an `Integer`.
|
201
|
+
# @raise [ArgumentError] if `day` is less than 1 or greater than 365.
|
202
|
+
def initialize(day, transition_at = 0)
|
203
|
+
super(day, transition_at)
|
204
|
+
raise ArgumentError, 'Invalid day' unless day >= 1 && day <= 365
|
205
|
+
end
|
206
|
+
|
207
|
+
# @return [Boolean] `true` if the day specified by this transition is the
|
208
|
+
# first in the year (a day number of 1), otherwise `false`.
|
209
|
+
def is_always_first_day_of_year?
|
210
|
+
seconds == 86400
|
211
|
+
end
|
212
|
+
|
213
|
+
# @return [Boolean] `true` if the day specified by this transition is the
|
214
|
+
# last in the year (a day number of 365), otherwise `false`.
|
215
|
+
def is_always_last_day_of_year?
|
216
|
+
seconds == YEAR
|
217
|
+
end
|
218
|
+
|
219
|
+
# Determines if this {JulianDayOfYearTransitionRule} is equal to another
|
220
|
+
# instance.
|
221
|
+
#
|
222
|
+
# @param r [Object] the instance to test for equality.
|
223
|
+
# @return [Boolean] `true` if `r` is a {JulianDayOfYearTransitionRule} with
|
224
|
+
# the same {transition_at} and day as this
|
225
|
+
# {JulianDayOfYearTransitionRule}, otherwise `false`.
|
226
|
+
def ==(r)
|
227
|
+
super(r) && r.kind_of?(JulianDayOfYearTransitionRule)
|
228
|
+
end
|
229
|
+
alias eql? ==
|
230
|
+
|
231
|
+
protected
|
232
|
+
|
233
|
+
# Returns a `Time` representing midnight local time on the day specified by
|
234
|
+
# the rule for the given offset and year.
|
235
|
+
#
|
236
|
+
# @param offset [TimezoneOffset] the current offset at the time of the
|
237
|
+
# transition.
|
238
|
+
# @param year [Integer] the year in which the transition occurs.
|
239
|
+
# @return [Time] midnight local time on the day specified by the rule for
|
240
|
+
# the given offset and year.
|
241
|
+
def get_day(offset, year)
|
242
|
+
# Returns 1 March on non-leap years.
|
243
|
+
leap = Time.new(year, 2, 29, 0, 0, 0, offset.observed_utc_offset)
|
244
|
+
diff = seconds - LEAP
|
245
|
+
diff += 86400 if diff >= 0 && leap.mday == 29
|
246
|
+
leap + diff
|
247
|
+
end
|
248
|
+
|
249
|
+
# (see TransitionRule#hash_args)
|
250
|
+
def hash_args
|
251
|
+
[JulianDayOfYearTransitionRule] + super
|
252
|
+
end
|
253
|
+
end
|
254
|
+
private_constant :JulianDayOfYearTransitionRule
|
255
|
+
|
256
|
+
# A base class for rules that transition on a particular day of week of a
|
257
|
+
# given week (subclasses specify which week of the month).
|
258
|
+
#
|
259
|
+
# @abstract
|
260
|
+
# @private
|
261
|
+
class DayOfWeekTransitionRule < TransitionRule #:nodoc:
|
262
|
+
# Initializes a new {DayOfWeekTransitionRule}.
|
263
|
+
#
|
264
|
+
# @param month [Integer] the month of the year when the transition occurs.
|
265
|
+
# @param day_of_week [Integer] the day of the week when the transition
|
266
|
+
# occurs. 0 is Sunday, 6 is Saturday.
|
267
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
268
|
+
# time at which the transition occurs.
|
269
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
270
|
+
# @raise [ArgumentError] if `month` is not an `Integer`.
|
271
|
+
# @raise [ArgumentError] if `month` is less than 1 or greater than 12.
|
272
|
+
# @raise [ArgumentError] if `day_of_week` is not an `Integer`.
|
273
|
+
# @raise [ArgumentError] if `day_of_week` is less than 0 or greater than 6.
|
274
|
+
def initialize(month, day_of_week, transition_at)
|
275
|
+
super(transition_at)
|
276
|
+
raise ArgumentError, 'Invalid month' unless month.kind_of?(Integer) && month >= 1 && month <= 12
|
277
|
+
raise ArgumentError, 'Invalid day_of_week' unless day_of_week.kind_of?(Integer) && day_of_week >= 0 && day_of_week <= 6
|
278
|
+
@month = month
|
279
|
+
@day_of_week = day_of_week
|
280
|
+
end
|
281
|
+
|
282
|
+
# @return [Boolean] `false`.
|
283
|
+
def is_always_first_day_of_year?
|
284
|
+
false
|
285
|
+
end
|
286
|
+
|
287
|
+
# @return [Boolean] `false`.
|
288
|
+
def is_always_last_day_of_year?
|
289
|
+
false
|
290
|
+
end
|
291
|
+
|
292
|
+
# Determines if this {DayOfWeekTransitionRule} is equal to another
|
293
|
+
# instance.
|
294
|
+
#
|
295
|
+
# @param r [Object] the instance to test for equality.
|
296
|
+
# @return [Boolean] `true` if `r` is a {DayOfWeekTransitionRule} with the
|
297
|
+
# same {transition_at}, month and day of week as this
|
298
|
+
# {DayOfWeekTransitionRule}, otherwise `false`.
|
299
|
+
def ==(r)
|
300
|
+
super(r) && r.kind_of?(DayOfWeekTransitionRule) && @month == r.month && @day_of_week == r.day_of_week
|
301
|
+
end
|
302
|
+
alias eql? ==
|
303
|
+
|
304
|
+
protected
|
305
|
+
|
306
|
+
# @return [Integer] the month of the year (1 to 12).
|
307
|
+
attr_reader :month
|
308
|
+
|
309
|
+
# @return [Integer] the day of the week (0 to 6 for Sunday to Monday).
|
310
|
+
attr_reader :day_of_week
|
311
|
+
|
312
|
+
# (see TransitionRule#hash_args)
|
313
|
+
def hash_args
|
314
|
+
[@month, @day_of_week] + super
|
315
|
+
end
|
316
|
+
end
|
317
|
+
private_constant :DayOfWeekTransitionRule
|
318
|
+
|
319
|
+
# A rule that transitions on the nth occurrence of a particular day of week
|
320
|
+
# of a calendar month.
|
321
|
+
#
|
322
|
+
# @private
|
323
|
+
class DayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
|
324
|
+
# Initializes a new {DayOfMonthTransitionRule}.
|
325
|
+
#
|
326
|
+
# @param month [Integer] the month of the year when the transition occurs.
|
327
|
+
# @param week [Integer] the week of the month when the transition occurs (1
|
328
|
+
# to 4).
|
329
|
+
# @param day_of_week [Integer] the day of the week when the transition
|
330
|
+
# occurs. 0 is Sunday, 6 is Saturday.
|
331
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
332
|
+
# time at which the transition occurs.
|
333
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
334
|
+
# @raise [ArgumentError] if `month` is not an `Integer`.
|
335
|
+
# @raise [ArgumentError] if `month` is less than 1 or greater than 12.
|
336
|
+
# @raise [ArgumentError] if `week` is not an `Integer`.
|
337
|
+
# @raise [ArgumentError] if `week` is less than 1 or greater than 4.
|
338
|
+
# @raise [ArgumentError] if `day_of_week` is not an `Integer`.
|
339
|
+
# @raise [ArgumentError] if `day_of_week` is less than 0 or greater than 6.
|
340
|
+
def initialize(month, week, day_of_week, transition_at = 0)
|
341
|
+
super(month, day_of_week, transition_at)
|
342
|
+
raise ArgumentError, 'Invalid week' unless week.kind_of?(Integer) && week >= 1 && week <= 4
|
343
|
+
@offset_start = (week - 1) * 7 + 1
|
344
|
+
end
|
345
|
+
|
346
|
+
# Determines if this {DayOfMonthTransitionRule} is equal to another
|
347
|
+
# instance.
|
348
|
+
#
|
349
|
+
# @param r [Object] the instance to test for equality.
|
350
|
+
# @return [Boolean] `true` if `r` is a {DayOfMonthTransitionRule} with the
|
351
|
+
# same {transition_at}, month, week and day of week as this
|
352
|
+
# {DayOfMonthTransitionRule}, otherwise `false`.
|
353
|
+
def ==(r)
|
354
|
+
super(r) && r.kind_of?(DayOfMonthTransitionRule) && @offset_start == r.offset_start
|
355
|
+
end
|
356
|
+
alias eql? ==
|
357
|
+
|
358
|
+
protected
|
359
|
+
|
360
|
+
# @return [Integer] the day the week starts on for a month starting on a
|
361
|
+
# Sunday.
|
362
|
+
attr_reader :offset_start
|
363
|
+
|
364
|
+
# Returns a `Time` representing midnight local time on the day specified by
|
365
|
+
# the rule for the given offset and year.
|
366
|
+
#
|
367
|
+
# @param offset [TimezoneOffset] the current offset at the time of the
|
368
|
+
# transition.
|
369
|
+
# @param year [Integer] the year in which the transition occurs.
|
370
|
+
# @return [Time] midnight local time on the day specified by the rule for
|
371
|
+
# the given offset and year.
|
372
|
+
def get_day(offset, year)
|
373
|
+
candidate = Time.new(year, month, @offset_start, 0, 0, 0, offset.observed_utc_offset)
|
374
|
+
diff = day_of_week - candidate.wday
|
375
|
+
|
376
|
+
if diff < 0
|
377
|
+
candidate + (7 + diff) * 86400
|
378
|
+
elsif diff > 0
|
379
|
+
candidate + diff * 86400
|
380
|
+
else
|
381
|
+
candidate
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# (see TransitionRule#hash_args)
|
386
|
+
def hash_args
|
387
|
+
[@offset_start] + super
|
388
|
+
end
|
389
|
+
end
|
390
|
+
private_constant :DayOfMonthTransitionRule
|
391
|
+
|
392
|
+
# A rule that transitions on the last occurrence of a particular day of week
|
393
|
+
# of a calendar month.
|
394
|
+
#
|
395
|
+
# @private
|
396
|
+
class LastDayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
|
397
|
+
# Initializes a new {LastDayOfMonthTransitionRule}.
|
398
|
+
#
|
399
|
+
# @param month [Integer] the month of the year when the transition occurs.
|
400
|
+
# @param day_of_week [Integer] the day of the week when the transition
|
401
|
+
# occurs. 0 is Sunday, 6 is Saturday.
|
402
|
+
# @param transition_at [Integer] the time in seconds after midnight local
|
403
|
+
# time at which the transition occurs.
|
404
|
+
# @raise [ArgumentError] if `transition_at` is not an `Integer`.
|
405
|
+
# @raise [ArgumentError] if `month` is not an `Integer`.
|
406
|
+
# @raise [ArgumentError] if `month` is less than 1 or greater than 12.
|
407
|
+
# @raise [ArgumentError] if `day_of_week` is not an `Integer`.
|
408
|
+
# @raise [ArgumentError] if `day_of_week` is less than 0 or greater than 6.
|
409
|
+
def initialize(month, day_of_week, transition_at = 0)
|
410
|
+
super(month, day_of_week, transition_at)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Determines if this {LastDayOfMonthTransitionRule} is equal to another
|
414
|
+
# instance.
|
415
|
+
#
|
416
|
+
# @param r [Object] the instance to test for equality.
|
417
|
+
# @return [Boolean] `true` if `r` is a {LastDayOfMonthTransitionRule} with
|
418
|
+
# the same {transition_at}, month and day of week as this
|
419
|
+
# {LastDayOfMonthTransitionRule}, otherwise `false`.
|
420
|
+
def ==(r)
|
421
|
+
super(r) && r.kind_of?(LastDayOfMonthTransitionRule)
|
422
|
+
end
|
423
|
+
alias eql? ==
|
424
|
+
|
425
|
+
protected
|
426
|
+
|
427
|
+
# Returns a `Time` representing midnight local time on the day specified by
|
428
|
+
# the rule for the given offset and year.
|
429
|
+
#
|
430
|
+
# @param offset [TimezoneOffset] the current offset at the time of the
|
431
|
+
# transition.
|
432
|
+
# @param year [Integer] the year in which the transition occurs.
|
433
|
+
# @return [Time] midnight local time on the day specified by the rule for
|
434
|
+
# the given offset and year.
|
435
|
+
def get_day(offset, year)
|
436
|
+
next_month = month + 1
|
437
|
+
if next_month == 13
|
438
|
+
year += 1
|
439
|
+
next_month = 1
|
440
|
+
end
|
441
|
+
|
442
|
+
candidate = Time.new(year, next_month, 1, 0, 0, 0, offset.observed_utc_offset) - 86400
|
443
|
+
diff = candidate.wday - day_of_week
|
444
|
+
|
445
|
+
if diff < 0
|
446
|
+
candidate - (diff + 7) * 86400
|
447
|
+
elsif diff > 0
|
448
|
+
candidate - diff * 86400
|
449
|
+
else
|
450
|
+
candidate
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
private_constant :LastDayOfMonthTransitionRule
|
455
|
+
end
|
data/lib/tzinfo/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tzinfo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Philip Ross
|
@@ -29,7 +29,7 @@ cert_chain:
|
|
29
29
|
J3Zn/kSTjTekiaspyGbczC3PUaeJNxr+yCvR4sk71Xmk/GaKKGOHedJ1uj/LAXrA
|
30
30
|
MR0mpl7b8zCg0PFC1J73uw==
|
31
31
|
-----END CERTIFICATE-----
|
32
|
-
date: 2020-
|
32
|
+
date: 2020-11-08 00:00:00.000000000 Z
|
33
33
|
dependencies:
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
35
|
name: concurrent-ruby
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- LICENSE
|
61
61
|
- README.md
|
62
62
|
- lib/tzinfo.rb
|
63
|
+
- lib/tzinfo/annual_rules.rb
|
63
64
|
- lib/tzinfo/country.rb
|
64
65
|
- lib/tzinfo/country_timezone.rb
|
65
66
|
- lib/tzinfo/data_source.rb
|
@@ -68,6 +69,7 @@ files:
|
|
68
69
|
- lib/tzinfo/data_sources/country_info.rb
|
69
70
|
- lib/tzinfo/data_sources/data_timezone_info.rb
|
70
71
|
- lib/tzinfo/data_sources/linked_timezone_info.rb
|
72
|
+
- lib/tzinfo/data_sources/posix_time_zone_parser.rb
|
71
73
|
- lib/tzinfo/data_sources/ruby_data_source.rb
|
72
74
|
- lib/tzinfo/data_sources/timezone_info.rb
|
73
75
|
- lib/tzinfo/data_sources/transitions_data_timezone_info.rb
|
@@ -101,6 +103,7 @@ files:
|
|
101
103
|
- lib/tzinfo/timezone_period.rb
|
102
104
|
- lib/tzinfo/timezone_proxy.rb
|
103
105
|
- lib/tzinfo/timezone_transition.rb
|
106
|
+
- lib/tzinfo/transition_rule.rb
|
104
107
|
- lib/tzinfo/transitions_timezone_period.rb
|
105
108
|
- lib/tzinfo/untaint_ext.rb
|
106
109
|
- lib/tzinfo/version.rb
|
@@ -108,7 +111,12 @@ files:
|
|
108
111
|
homepage: https://tzinfo.github.io
|
109
112
|
licenses:
|
110
113
|
- MIT
|
111
|
-
metadata:
|
114
|
+
metadata:
|
115
|
+
bug_tracker_uri: https://github.com/tzinfo/tzinfo/issues
|
116
|
+
changelog_uri: https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md
|
117
|
+
documentation_uri: https://rubydoc.info/gems/tzinfo/2.0.3
|
118
|
+
homepage_uri: https://tzinfo.github.io
|
119
|
+
source_code_uri: https://github.com/tzinfo/tzinfo/tree/v2.0.3
|
112
120
|
post_install_message:
|
113
121
|
rdoc_options:
|
114
122
|
- "--title"
|
@@ -128,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
136
|
- !ruby/object:Gem::Version
|
129
137
|
version: '0'
|
130
138
|
requirements: []
|
131
|
-
rubygems_version: 3.1.
|
139
|
+
rubygems_version: 3.1.4
|
132
140
|
signing_key:
|
133
141
|
specification_version: 4
|
134
142
|
summary: Time Zone Library
|
metadata.gz.sig
CHANGED
Binary file
|