tzinfo 1.2.5 → 2.0.5
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/.yardopts +3 -0
- data/CHANGES.md +607 -377
- data/LICENSE +13 -13
- data/README.md +368 -113
- data/lib/tzinfo/annual_rules.rb +71 -0
- data/lib/tzinfo/country.rb +141 -129
- data/lib/tzinfo/country_timezone.rb +70 -112
- data/lib/tzinfo/data_source.rb +400 -144
- data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
- data/lib/tzinfo/data_sources/country_info.rb +42 -0
- data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
- data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
- data/lib/tzinfo/data_sources/posix_time_zone_parser.rb +181 -0
- data/lib/tzinfo/data_sources/ruby_data_source.rb +145 -0
- data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
- data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
- data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +596 -0
- data/lib/tzinfo/data_sources/zoneinfo_reader.rb +486 -0
- data/lib/tzinfo/data_sources.rb +8 -0
- data/lib/tzinfo/data_timezone.rb +33 -47
- data/lib/tzinfo/datetime_with_offset.rb +153 -0
- data/lib/tzinfo/format1/country_definer.rb +17 -0
- data/lib/tzinfo/format1/country_index_definition.rb +64 -0
- data/lib/tzinfo/format1/timezone_definer.rb +64 -0
- data/lib/tzinfo/format1/timezone_definition.rb +39 -0
- data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
- data/lib/tzinfo/format1.rb +10 -0
- data/lib/tzinfo/format2/country_definer.rb +68 -0
- data/lib/tzinfo/format2/country_index_definer.rb +68 -0
- data/lib/tzinfo/format2/country_index_definition.rb +46 -0
- data/lib/tzinfo/format2/timezone_definer.rb +94 -0
- data/lib/tzinfo/format2/timezone_definition.rb +73 -0
- data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
- data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
- data/lib/tzinfo/format2.rb +10 -0
- data/lib/tzinfo/info_timezone.rb +26 -21
- data/lib/tzinfo/linked_timezone.rb +33 -52
- data/lib/tzinfo/offset_timezone_period.rb +42 -0
- data/lib/tzinfo/string_deduper.rb +118 -0
- data/lib/tzinfo/time_with_offset.rb +154 -0
- data/lib/tzinfo/timestamp.rb +552 -0
- data/lib/tzinfo/timestamp_with_offset.rb +85 -0
- data/lib/tzinfo/timezone.rb +989 -498
- data/lib/tzinfo/timezone_offset.rb +84 -74
- data/lib/tzinfo/timezone_period.rb +151 -217
- data/lib/tzinfo/timezone_proxy.rb +70 -79
- data/lib/tzinfo/timezone_transition.rb +77 -109
- data/lib/tzinfo/transition_rule.rb +455 -0
- data/lib/tzinfo/transitions_timezone_period.rb +63 -0
- data/lib/tzinfo/untaint_ext.rb +18 -0
- data/lib/tzinfo/version.rb +7 -0
- data/lib/tzinfo/with_offset.rb +61 -0
- data/lib/tzinfo.rb +74 -29
- data.tar.gz.sig +0 -0
- metadata +72 -122
- metadata.gz.sig +0 -0
- data/Rakefile +0 -107
- data/lib/tzinfo/country_index_definition.rb +0 -31
- data/lib/tzinfo/country_info.rb +0 -42
- data/lib/tzinfo/data_timezone_info.rb +0 -55
- data/lib/tzinfo/linked_timezone_info.rb +0 -26
- data/lib/tzinfo/offset_rationals.rb +0 -77
- data/lib/tzinfo/ruby_core_support.rb +0 -146
- data/lib/tzinfo/ruby_country_info.rb +0 -74
- data/lib/tzinfo/ruby_data_source.rb +0 -136
- data/lib/tzinfo/time_or_datetime.rb +0 -340
- data/lib/tzinfo/timezone_definition.rb +0 -36
- data/lib/tzinfo/timezone_index_definition.rb +0 -54
- data/lib/tzinfo/timezone_info.rb +0 -30
- data/lib/tzinfo/timezone_transition_definition.rb +0 -104
- data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
- data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
- data/lib/tzinfo/zoneinfo_data_source.rb +0 -488
- data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -296
- data/test/tc_country.rb +0 -234
- data/test/tc_country_index_definition.rb +0 -69
- data/test/tc_country_info.rb +0 -16
- data/test/tc_country_timezone.rb +0 -173
- data/test/tc_data_source.rb +0 -218
- data/test/tc_data_timezone.rb +0 -99
- data/test/tc_data_timezone_info.rb +0 -18
- data/test/tc_info_timezone.rb +0 -34
- data/test/tc_linked_timezone.rb +0 -155
- data/test/tc_linked_timezone_info.rb +0 -23
- data/test/tc_offset_rationals.rb +0 -23
- data/test/tc_ruby_core_support.rb +0 -168
- data/test/tc_ruby_country_info.rb +0 -110
- data/test/tc_ruby_data_source.rb +0 -143
- data/test/tc_time_or_datetime.rb +0 -654
- data/test/tc_timezone.rb +0 -1350
- data/test/tc_timezone_definition.rb +0 -113
- data/test/tc_timezone_index_definition.rb +0 -73
- data/test/tc_timezone_info.rb +0 -11
- data/test/tc_timezone_london.rb +0 -143
- data/test/tc_timezone_melbourne.rb +0 -142
- data/test/tc_timezone_new_york.rb +0 -142
- data/test/tc_timezone_offset.rb +0 -126
- data/test/tc_timezone_period.rb +0 -555
- data/test/tc_timezone_proxy.rb +0 -136
- data/test/tc_timezone_transition.rb +0 -366
- data/test/tc_timezone_transition_definition.rb +0 -295
- data/test/tc_timezone_utc.rb +0 -27
- data/test/tc_transition_data_timezone_info.rb +0 -423
- data/test/tc_zoneinfo_country_info.rb +0 -78
- data/test/tc_zoneinfo_data_source.rb +0 -1195
- data/test/tc_zoneinfo_timezone_info.rb +0 -1232
- data/test/test_utils.rb +0 -163
- data/test/ts_all.rb +0 -7
- data/test/ts_all_ruby.rb +0 -5
- data/test/ts_all_zoneinfo.rb +0 -7
- data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
- data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -315
- data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -218
- data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
- data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
- data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
- data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
- data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -261
- data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -186
- data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -321
- data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -265
- data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -220
- data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
- data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -927
- data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -596
- data/test/tzinfo-data/tzinfo/data/version.rb +0 -14
- data/test/tzinfo-data/tzinfo/data.rb +0 -8
- data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
- data/test/zoneinfo/America/New_York +0 -0
- data/test/zoneinfo/Australia/Melbourne +0 -0
- data/test/zoneinfo/EST +0 -0
- data/test/zoneinfo/Etc/UTC +0 -0
- data/test/zoneinfo/Europe/Amsterdam +0 -0
- data/test/zoneinfo/Europe/Andorra +0 -0
- data/test/zoneinfo/Europe/London +0 -0
- data/test/zoneinfo/Europe/Paris +0 -0
- data/test/zoneinfo/Europe/Prague +0 -0
- data/test/zoneinfo/Factory +0 -0
- data/test/zoneinfo/iso3166.tab +0 -275
- data/test/zoneinfo/leapseconds +0 -61
- data/test/zoneinfo/posix/Europe/London +0 -0
- data/test/zoneinfo/posixrules +0 -0
- data/test/zoneinfo/right/Europe/London +0 -0
- data/test/zoneinfo/zone.tab +0 -439
- data/test/zoneinfo/zone1970.tab +0 -369
- data/tzinfo.gemspec +0 -21
@@ -0,0 +1,486 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TZInfo
|
5
|
+
# Use send as a workaround for erroneous 'wrong number of arguments' errors
|
6
|
+
# with JRuby 9.0.5.0 when calling methods with Java implementations. See #114.
|
7
|
+
send(:using, UntaintExt) if TZInfo.const_defined?(:UntaintExt)
|
8
|
+
|
9
|
+
module DataSources
|
10
|
+
# An {InvalidZoneinfoFile} exception is raised if an attempt is made to load
|
11
|
+
# an invalid zoneinfo file.
|
12
|
+
class InvalidZoneinfoFile < StandardError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Reads compiled zoneinfo TZif (\0, 2 or 3) files.
|
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
|
+
|
23
|
+
# Initializes a new {ZoneinfoReader}.
|
24
|
+
#
|
25
|
+
# @param posix_tz_parser [PosixTimeZoneParser] a {PosixTimeZoneParser}
|
26
|
+
# instance to use to parse POSIX-style TZ strings.
|
27
|
+
# @param string_deduper [StringDeduper] a {StringDeduper} instance to use
|
28
|
+
# to dedupe abbreviations.
|
29
|
+
def initialize(posix_tz_parser, string_deduper)
|
30
|
+
@posix_tz_parser = posix_tz_parser
|
31
|
+
@string_deduper = string_deduper
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reads a zoneinfo structure from the given path. Returns either a
|
35
|
+
# {TimezoneOffset} that is constantly observed or an `Array`
|
36
|
+
# {TimezoneTransition}s.
|
37
|
+
#
|
38
|
+
# @param file_path [String] the path of a zoneinfo file.
|
39
|
+
# @return [Object] either a {TimezoneOffset} or an `Array` of
|
40
|
+
# {TimezoneTransition}s.
|
41
|
+
# @raise [SecurityError] if safe mode is enabled and `file_path` is
|
42
|
+
# tainted.
|
43
|
+
# @raise [InvalidZoneinfoFile] if `file_path`` does not refer to a valid
|
44
|
+
# zoneinfo file.
|
45
|
+
def read(file_path)
|
46
|
+
File.open(file_path, 'rb') { |file| parse(file) }
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Translates an unsigned 32-bit integer (as returned by unpack) to signed
|
52
|
+
# 32-bit.
|
53
|
+
#
|
54
|
+
# @param long [Integer] an unsigned 32-bit integer.
|
55
|
+
# @return [Integer] {long} translated to signed 32-bit.
|
56
|
+
def make_signed_int32(long)
|
57
|
+
long >= 0x80000000 ? long - 0x100000000 : long
|
58
|
+
end
|
59
|
+
|
60
|
+
# Translates a pair of unsigned 32-bit integers (as returned by unpack,
|
61
|
+
# most significant first) to a signed 64-bit integer.
|
62
|
+
#
|
63
|
+
# @param high [Integer] the most significant 32-bits.
|
64
|
+
# @param low [Integer] the least significant 32-bits.
|
65
|
+
# @return [Integer] {high} and {low} combined and translated to signed
|
66
|
+
# 64-bit.
|
67
|
+
def make_signed_int64(high, low)
|
68
|
+
unsigned = (high << 32) | low
|
69
|
+
unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reads the given number of bytes from the given file and checks that the
|
73
|
+
# correct number of bytes could be read.
|
74
|
+
#
|
75
|
+
# @param file [IO] the file to read from.
|
76
|
+
# @param bytes [Integer] the number of bytes to read.
|
77
|
+
# @return [String] the bytes that were read.
|
78
|
+
# @raise [InvalidZoneinfoFile] if the number of bytes available didn't
|
79
|
+
# match the number requested.
|
80
|
+
def check_read(file, bytes)
|
81
|
+
result = file.read(bytes)
|
82
|
+
|
83
|
+
unless result && result.length == bytes
|
84
|
+
raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
|
85
|
+
end
|
86
|
+
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
# Zoneinfo files don't include the offset from standard time (std_offset)
|
91
|
+
# for DST periods. Derive the base offset (base_utc_offset) where DST is
|
92
|
+
# observed from either the previous or next non-DST period.
|
93
|
+
#
|
94
|
+
# @param transitions [Array<Hash>] an `Array` of transition hashes.
|
95
|
+
# @param offsets [Array<Hash>] an `Array` of offset hashes.
|
96
|
+
# @return [Integer] the index of the offset to be used prior to the first
|
97
|
+
# transition.
|
98
|
+
def derive_offsets(transitions, offsets)
|
99
|
+
# The first non-DST offset (if there is one) is the offset observed
|
100
|
+
# before the first transition. Fall back to the first DST offset if
|
101
|
+
# there are no non-DST offsets.
|
102
|
+
first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
|
103
|
+
first_offset_index = first_non_dst_offset_index || 0
|
104
|
+
return first_offset_index if transitions.empty?
|
105
|
+
|
106
|
+
# Determine the base_utc_offset of the next non-dst offset at each transition.
|
107
|
+
base_utc_offset_from_next = nil
|
108
|
+
|
109
|
+
transitions.reverse_each do |transition|
|
110
|
+
offset = offsets[transition[:offset]]
|
111
|
+
if offset[:is_dst]
|
112
|
+
transition[:base_utc_offset_from_next] = base_utc_offset_from_next if base_utc_offset_from_next
|
113
|
+
else
|
114
|
+
base_utc_offset_from_next = offset[:observed_utc_offset]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
base_utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:observed_utc_offset] : nil
|
119
|
+
defined_offsets = {}
|
120
|
+
|
121
|
+
transitions.each do |transition|
|
122
|
+
offset_index = transition[:offset]
|
123
|
+
offset = offsets[offset_index]
|
124
|
+
observed_utc_offset = offset[:observed_utc_offset]
|
125
|
+
|
126
|
+
if offset[:is_dst]
|
127
|
+
base_utc_offset_from_next = transition[:base_utc_offset_from_next]
|
128
|
+
|
129
|
+
difference_to_previous = (observed_utc_offset - (base_utc_offset_from_previous || observed_utc_offset)).abs
|
130
|
+
difference_to_next = (observed_utc_offset - (base_utc_offset_from_next || observed_utc_offset)).abs
|
131
|
+
|
132
|
+
base_utc_offset = if difference_to_previous == 3600
|
133
|
+
base_utc_offset_from_previous
|
134
|
+
elsif difference_to_next == 3600
|
135
|
+
base_utc_offset_from_next
|
136
|
+
elsif difference_to_previous > 0 && difference_to_next > 0
|
137
|
+
difference_to_previous < difference_to_next ? base_utc_offset_from_previous : base_utc_offset_from_next
|
138
|
+
elsif difference_to_previous > 0
|
139
|
+
base_utc_offset_from_previous
|
140
|
+
elsif difference_to_next > 0
|
141
|
+
base_utc_offset_from_next
|
142
|
+
else
|
143
|
+
# No difference, assume a 1 hour offset from standard time.
|
144
|
+
observed_utc_offset - 3600
|
145
|
+
end
|
146
|
+
|
147
|
+
if !offset[:base_utc_offset]
|
148
|
+
offset[:base_utc_offset] = base_utc_offset
|
149
|
+
defined_offsets[offset] = offset_index
|
150
|
+
elsif offset[:base_utc_offset] != base_utc_offset
|
151
|
+
# An earlier transition has already derived a different
|
152
|
+
# base_utc_offset. Define a new offset or reuse an existing identically
|
153
|
+
# defined offset.
|
154
|
+
new_offset = offset.dup
|
155
|
+
new_offset[:base_utc_offset] = base_utc_offset
|
156
|
+
|
157
|
+
offset_index = defined_offsets[new_offset]
|
158
|
+
|
159
|
+
unless offset_index
|
160
|
+
offsets << new_offset
|
161
|
+
offset_index = offsets.length - 1
|
162
|
+
defined_offsets[new_offset] = offset_index
|
163
|
+
end
|
164
|
+
|
165
|
+
transition[:offset] = offset_index
|
166
|
+
end
|
167
|
+
else
|
168
|
+
base_utc_offset_from_previous = observed_utc_offset
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
first_offset_index
|
173
|
+
end
|
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 do |t|
|
327
|
+
t.timestamp_value > last_defined.timestamp_value && !offset_matches_rule?(last_defined.offset, t.offset)
|
328
|
+
end
|
329
|
+
|
330
|
+
generated += (last_year + 1).upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
|
331
|
+
|
332
|
+
unless generated.empty?
|
333
|
+
transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, generated[0].previous_offset)
|
334
|
+
transitions.concat(generated)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Parses a zoneinfo file and returns either a {TimezoneOffset} that is
|
341
|
+
# constantly observed or an `Array` of {TimezoneTransition}s.
|
342
|
+
#
|
343
|
+
# @param file [IO] the file to be read.
|
344
|
+
# @return [Object] either a {TimezoneOffset} or an `Array` of
|
345
|
+
# {TimezoneTransition}s.
|
346
|
+
# @raise [InvalidZoneinfoFile] if the file is not a valid zoneinfo file.
|
347
|
+
def parse(file)
|
348
|
+
magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
|
349
|
+
check_read(file, 44).unpack('a4 a x15 NNNNNN')
|
350
|
+
|
351
|
+
if magic != 'TZif'
|
352
|
+
raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
|
353
|
+
end
|
354
|
+
|
355
|
+
if version == '2' || version == '3'
|
356
|
+
# Skip the first 32-bit section and read the header of the second 64-bit section
|
357
|
+
file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
|
358
|
+
|
359
|
+
prev_version = version
|
360
|
+
|
361
|
+
magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
|
362
|
+
check_read(file, 44).unpack('a4 a x15 NNNNNN')
|
363
|
+
|
364
|
+
unless magic == 'TZif' && (version == prev_version)
|
365
|
+
raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
|
366
|
+
end
|
367
|
+
|
368
|
+
using_64bit = true
|
369
|
+
elsif version != '3' && version != '2' && version != "\0"
|
370
|
+
raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
|
371
|
+
else
|
372
|
+
using_64bit = false
|
373
|
+
end
|
374
|
+
|
375
|
+
unless leapcnt == 0
|
376
|
+
raise InvalidZoneinfoFile, "The file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
|
377
|
+
end
|
378
|
+
|
379
|
+
transitions = if using_64bit
|
380
|
+
timecnt.times.map do |i|
|
381
|
+
high, low = check_read(file, 8).unpack('NN'.freeze)
|
382
|
+
transition_time = make_signed_int64(high, low)
|
383
|
+
{at: transition_time}
|
384
|
+
end
|
385
|
+
else
|
386
|
+
timecnt.times.map do |i|
|
387
|
+
transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
|
388
|
+
{at: transition_time}
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
check_read(file, timecnt).unpack('C*'.freeze).each_with_index do |localtime_type, i|
|
393
|
+
raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'." if localtime_type >= typecnt
|
394
|
+
transitions[i][:offset] = localtime_type
|
395
|
+
end
|
396
|
+
|
397
|
+
offsets = typecnt.times.map do |i|
|
398
|
+
gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
|
399
|
+
gmtoff = make_signed_int32(gmtoff)
|
400
|
+
isdst = isdst == 1
|
401
|
+
{observed_utc_offset: gmtoff, is_dst: isdst, abbr_index: abbrind}
|
402
|
+
end
|
403
|
+
|
404
|
+
abbrev = check_read(file, charcnt)
|
405
|
+
|
406
|
+
if using_64bit
|
407
|
+
# Skip to the POSIX-style TZ string.
|
408
|
+
file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
|
409
|
+
tz_string_start = check_read(file, 1)
|
410
|
+
raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
|
411
|
+
tz_string = file.readline("\n").force_encoding(Encoding::UTF_8)
|
412
|
+
raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
|
413
|
+
|
414
|
+
begin
|
415
|
+
rules = @posix_tz_parser.parse(tz_string)
|
416
|
+
rescue InvalidPosixTimeZone => e
|
417
|
+
raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
|
418
|
+
end
|
419
|
+
else
|
420
|
+
rules = nil
|
421
|
+
end
|
422
|
+
|
423
|
+
# Derive the offsets from standard time (std_offset).
|
424
|
+
first_offset_index = derive_offsets(transitions, offsets)
|
425
|
+
|
426
|
+
offsets = offsets.map do |o|
|
427
|
+
observed_utc_offset = o[:observed_utc_offset]
|
428
|
+
base_utc_offset = o[:base_utc_offset]
|
429
|
+
|
430
|
+
if base_utc_offset
|
431
|
+
# DST offset with base_utc_offset derived by derive_offsets.
|
432
|
+
std_offset = observed_utc_offset - base_utc_offset
|
433
|
+
elsif o[:is_dst]
|
434
|
+
# DST offset unreferenced by a transition (offset in use before the
|
435
|
+
# first transition). No derived base UTC offset, so assume 1 hour
|
436
|
+
# DST.
|
437
|
+
base_utc_offset = observed_utc_offset - 3600
|
438
|
+
std_offset = 3600
|
439
|
+
else
|
440
|
+
# Non-DST offset.
|
441
|
+
base_utc_offset = observed_utc_offset
|
442
|
+
std_offset = 0
|
443
|
+
end
|
444
|
+
|
445
|
+
abbrev_start = o[:abbr_index]
|
446
|
+
raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'." unless abbrev_start < abbrev.length
|
447
|
+
|
448
|
+
abbrev_end = abbrev.index("\0", abbrev_start)
|
449
|
+
raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'." unless abbrev_end
|
450
|
+
|
451
|
+
abbr = @string_deduper.dedupe(abbrev[abbrev_start...abbrev_end].force_encoding(Encoding::UTF_8).untaint)
|
452
|
+
|
453
|
+
TimezoneOffset.new(base_utc_offset, std_offset, abbr)
|
454
|
+
end
|
455
|
+
|
456
|
+
first_offset = offsets[first_offset_index]
|
457
|
+
|
458
|
+
|
459
|
+
if transitions.empty?
|
460
|
+
if rules
|
461
|
+
apply_rules_without_transitions(file, first_offset, rules)
|
462
|
+
else
|
463
|
+
first_offset
|
464
|
+
end
|
465
|
+
else
|
466
|
+
previous_offset = first_offset
|
467
|
+
previous_at = nil
|
468
|
+
|
469
|
+
transitions = transitions.map do |t|
|
470
|
+
offset = offsets[t[:offset]]
|
471
|
+
at = t[:at]
|
472
|
+
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
|
473
|
+
tt = TimezoneTransition.new(offset, previous_offset, at)
|
474
|
+
previous_offset = offset
|
475
|
+
previous_at = at
|
476
|
+
tt
|
477
|
+
end
|
478
|
+
|
479
|
+
apply_rules_with_transitions(file, transitions, offsets, rules) if rules
|
480
|
+
transitions
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
private_constant :ZoneinfoReader
|
485
|
+
end
|
486
|
+
end
|
data/lib/tzinfo/data_timezone.rb
CHANGED
@@ -1,56 +1,42 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
1
4
|
module TZInfo
|
5
|
+
# Represents time zones that are defined by rules that set out when
|
6
|
+
# transitions occur.
|
7
|
+
class DataTimezone < InfoTimezone
|
8
|
+
# (see Timezone#period_for)
|
9
|
+
def period_for(time)
|
10
|
+
raise ArgumentError, 'time must be specified' unless time
|
11
|
+
timestamp = Timestamp.for(time)
|
12
|
+
raise ArgumentError, 'time must have a specified utc_offset' unless timestamp.utc_offset
|
13
|
+
info.period_for(timestamp)
|
14
|
+
end
|
2
15
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# Returns the TimezonePeriod for the given UTC time. utc can either be
|
9
|
-
# a DateTime, Time or integer timestamp (Time.to_i). Any timezone
|
10
|
-
# information in utc is ignored (it is treated as a UTC time).
|
11
|
-
#
|
12
|
-
# If no TimezonePeriod could be found, PeriodNotFound is raised.
|
13
|
-
def period_for_utc(utc)
|
14
|
-
info.period_for_utc(utc)
|
16
|
+
# (see Timezone#periods_for_local)
|
17
|
+
def periods_for_local(local_time)
|
18
|
+
raise ArgumentError, 'local_time must be specified' unless local_time
|
19
|
+
info.periods_for_local(Timestamp.for(local_time, :ignore))
|
15
20
|
end
|
16
|
-
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
|
22
|
+
# (see Timezone#transitions_up_to)
|
23
|
+
def transitions_up_to(to, from = nil)
|
24
|
+
raise ArgumentError, 'to must be specified' unless to
|
25
|
+
to_timestamp = Timestamp.for(to)
|
26
|
+
from_timestamp = from && Timestamp.for(from)
|
27
|
+
|
28
|
+
begin
|
29
|
+
info.transitions_up_to(to_timestamp, from_timestamp)
|
30
|
+
rescue ArgumentError => e
|
31
|
+
raise ArgumentError, e.message.gsub('_timestamp', '')
|
32
|
+
end
|
23
33
|
end
|
24
|
-
|
25
|
-
# Returns
|
26
|
-
# where the UTC offset of the timezone changes.
|
27
|
-
#
|
28
|
-
# Transitions are returned up to a given date and time up to a given date
|
29
|
-
# and time, specified in UTC (utc_to).
|
30
|
-
#
|
31
|
-
# A from date and time may also be supplied using the utc_from parameter
|
32
|
-
# (also specified in UTC). If utc_from is not nil, only transitions from
|
33
|
-
# that date and time onwards will be returned.
|
34
|
-
#
|
35
|
-
# Comparisons with utc_to are exclusive. Comparisons with utc_from are
|
36
|
-
# inclusive. If a transition falls precisely on utc_to, it will be excluded.
|
37
|
-
# If a transition falls on utc_from, it will be included.
|
38
|
-
#
|
39
|
-
# Transitions returned are ordered by when they occur, from earliest to
|
40
|
-
# latest.
|
41
|
-
#
|
42
|
-
# utc_to and utc_from can be specified using either DateTime, Time or
|
43
|
-
# integer timestamps (Time.to_i).
|
34
|
+
|
35
|
+
# Returns the canonical {Timezone} instance for this {DataTimezone}.
|
44
36
|
#
|
45
|
-
#
|
46
|
-
# transitions_up_to raises an ArgumentError exception.
|
47
|
-
def transitions_up_to(utc_to, utc_from = nil)
|
48
|
-
info.transitions_up_to(utc_to, utc_from)
|
49
|
-
end
|
50
|
-
|
51
|
-
# Returns the canonical zone for this Timezone.
|
37
|
+
# For a {DataTimezone}, this is always `self`.
|
52
38
|
#
|
53
|
-
#
|
39
|
+
# @return [Timezone] `self`.
|
54
40
|
def canonical_zone
|
55
41
|
self
|
56
42
|
end
|