tzinfo 2.0.2 → 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![RubyGems](https://img.shields.io/gem/v/tzinfo)](https://rubygems.org/gems/tzinfo) [![Travis CI Build](https://img.shields.io/travis/tzinfo/tzinfo?logo=travis)](https://travis-ci.
|
3
|
+
[![RubyGems](https://img.shields.io/gem/v/tzinfo)](https://rubygems.org/gems/tzinfo) [![Travis CI Build](https://img.shields.io/travis/com/tzinfo/tzinfo?logo=travis)](https://travis-ci.com/github/tzinfo/tzinfo) [![AppVeyor Build](https://img.shields.io/appveyor/build/philr/tzinfo?logo=appveyor)](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
|