tzinfo 1.2.11 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.yardopts +3 -0
  4. data/CHANGES.md +580 -394
  5. data/LICENSE +12 -12
  6. data/README.md +368 -114
  7. data/lib/tzinfo/annual_rules.rb +32 -12
  8. data/lib/tzinfo/country.rb +141 -129
  9. data/lib/tzinfo/country_timezone.rb +70 -112
  10. data/lib/tzinfo/data_source.rb +400 -144
  11. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  12. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  13. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  14. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  15. data/lib/tzinfo/data_sources/posix_time_zone_parser.rb +177 -0
  16. data/lib/tzinfo/data_sources/ruby_data_source.rb +141 -0
  17. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  18. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  19. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +592 -0
  20. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +482 -0
  21. data/lib/tzinfo/data_sources.rb +8 -0
  22. data/lib/tzinfo/data_timezone.rb +33 -47
  23. data/lib/tzinfo/datetime_with_offset.rb +153 -0
  24. data/lib/tzinfo/format1/country_definer.rb +17 -0
  25. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  26. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  27. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  28. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  29. data/lib/tzinfo/format1.rb +10 -0
  30. data/lib/tzinfo/format2/country_definer.rb +68 -0
  31. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  32. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  33. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  34. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  35. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  36. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
  37. data/lib/tzinfo/format2.rb +10 -0
  38. data/lib/tzinfo/info_timezone.rb +26 -21
  39. data/lib/tzinfo/linked_timezone.rb +33 -52
  40. data/lib/tzinfo/offset_timezone_period.rb +42 -0
  41. data/lib/tzinfo/ruby_core_support.rb +25 -163
  42. data/lib/tzinfo/string_deduper.rb +118 -0
  43. data/lib/tzinfo/time_with_offset.rb +154 -0
  44. data/lib/tzinfo/timestamp.rb +552 -0
  45. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  46. data/lib/tzinfo/timezone.rb +989 -502
  47. data/lib/tzinfo/timezone_offset.rb +84 -74
  48. data/lib/tzinfo/timezone_period.rb +151 -217
  49. data/lib/tzinfo/timezone_proxy.rb +70 -79
  50. data/lib/tzinfo/timezone_transition.rb +77 -109
  51. data/lib/tzinfo/transition_rule.rb +207 -77
  52. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  53. data/lib/tzinfo/version.rb +7 -0
  54. data/lib/tzinfo/with_offset.rb +61 -0
  55. data/lib/tzinfo.rb +78 -40
  56. data.tar.gz.sig +0 -0
  57. metadata +48 -103
  58. metadata.gz.sig +1 -3
  59. data/Rakefile +0 -107
  60. data/lib/tzinfo/country_index_definition.rb +0 -31
  61. data/lib/tzinfo/country_info.rb +0 -42
  62. data/lib/tzinfo/data_timezone_info.rb +0 -55
  63. data/lib/tzinfo/linked_timezone_info.rb +0 -26
  64. data/lib/tzinfo/offset_rationals.rb +0 -77
  65. data/lib/tzinfo/posix_time_zone_parser.rb +0 -136
  66. data/lib/tzinfo/ruby_country_info.rb +0 -74
  67. data/lib/tzinfo/ruby_data_source.rb +0 -136
  68. data/lib/tzinfo/time_or_datetime.rb +0 -351
  69. data/lib/tzinfo/timezone_definition.rb +0 -36
  70. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  71. data/lib/tzinfo/timezone_info.rb +0 -30
  72. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  73. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  74. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  75. data/lib/tzinfo/zoneinfo_data_source.rb +0 -504
  76. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -516
  77. data/test/assets/payload.rb +0 -1
  78. data/test/tc_annual_rules.rb +0 -95
  79. data/test/tc_country.rb +0 -240
  80. data/test/tc_country_index_definition.rb +0 -69
  81. data/test/tc_country_info.rb +0 -16
  82. data/test/tc_country_timezone.rb +0 -173
  83. data/test/tc_data_source.rb +0 -218
  84. data/test/tc_data_timezone.rb +0 -99
  85. data/test/tc_data_timezone_info.rb +0 -18
  86. data/test/tc_info_timezone.rb +0 -34
  87. data/test/tc_linked_timezone.rb +0 -155
  88. data/test/tc_linked_timezone_info.rb +0 -23
  89. data/test/tc_offset_rationals.rb +0 -23
  90. data/test/tc_posix_time_zone_parser.rb +0 -261
  91. data/test/tc_ruby_core_support.rb +0 -168
  92. data/test/tc_ruby_country_info.rb +0 -110
  93. data/test/tc_ruby_data_source.rb +0 -175
  94. data/test/tc_time_or_datetime.rb +0 -674
  95. data/test/tc_timezone.rb +0 -1361
  96. data/test/tc_timezone_definition.rb +0 -113
  97. data/test/tc_timezone_index_definition.rb +0 -73
  98. data/test/tc_timezone_info.rb +0 -11
  99. data/test/tc_timezone_london.rb +0 -143
  100. data/test/tc_timezone_melbourne.rb +0 -142
  101. data/test/tc_timezone_new_york.rb +0 -142
  102. data/test/tc_timezone_offset.rb +0 -126
  103. data/test/tc_timezone_period.rb +0 -555
  104. data/test/tc_timezone_proxy.rb +0 -136
  105. data/test/tc_timezone_transition.rb +0 -366
  106. data/test/tc_timezone_transition_definition.rb +0 -295
  107. data/test/tc_timezone_utc.rb +0 -27
  108. data/test/tc_transition_data_timezone_info.rb +0 -433
  109. data/test/tc_transition_rule.rb +0 -663
  110. data/test/tc_zoneinfo_country_info.rb +0 -78
  111. data/test/tc_zoneinfo_data_source.rb +0 -1226
  112. data/test/tc_zoneinfo_timezone_info.rb +0 -2149
  113. data/test/test_utils.rb +0 -214
  114. data/test/ts_all.rb +0 -7
  115. data/test/ts_all_ruby.rb +0 -5
  116. data/test/ts_all_zoneinfo.rb +0 -9
  117. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  118. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -327
  119. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -230
  120. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  121. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  122. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  123. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  124. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -273
  125. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -198
  126. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -333
  127. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -277
  128. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -235
  129. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  130. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -940
  131. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -609
  132. data/test/tzinfo-data/tzinfo/data/version.rb +0 -20
  133. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  134. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  135. data/test/zoneinfo/America/New_York +0 -0
  136. data/test/zoneinfo/Australia/Melbourne +0 -0
  137. data/test/zoneinfo/EST +0 -0
  138. data/test/zoneinfo/Etc/UTC +0 -0
  139. data/test/zoneinfo/Europe/Amsterdam +0 -0
  140. data/test/zoneinfo/Europe/Andorra +0 -0
  141. data/test/zoneinfo/Europe/London +0 -0
  142. data/test/zoneinfo/Europe/Paris +0 -0
  143. data/test/zoneinfo/Europe/Prague +0 -0
  144. data/test/zoneinfo/Factory +0 -0
  145. data/test/zoneinfo/iso3166.tab +0 -274
  146. data/test/zoneinfo/leapseconds +0 -78
  147. data/test/zoneinfo/posix/Europe/London +0 -0
  148. data/test/zoneinfo/posixrules +0 -0
  149. data/test/zoneinfo/right/Europe/London +0 -0
  150. data/test/zoneinfo/zone.tab +0 -452
  151. data/test/zoneinfo/zone1970.tab +0 -384
  152. data/tzinfo.gemspec +0 -21
@@ -0,0 +1,482 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ module DataSources
6
+ # An {InvalidZoneinfoFile} exception is raised if an attempt is made to load
7
+ # an invalid zoneinfo file.
8
+ class InvalidZoneinfoFile < StandardError
9
+ end
10
+
11
+ # Reads compiled zoneinfo TZif (\0, 2 or 3) files.
12
+ class ZoneinfoReader #:nodoc:
13
+ # The year to generate transitions up to.
14
+ #
15
+ # @private
16
+ GENERATE_UP_TO = Time.now.utc.year + 100
17
+ private_constant :GENERATE_UP_TO
18
+
19
+ # Initializes a new {ZoneinfoReader}.
20
+ #
21
+ # @param posix_tz_parser [PosixTimeZoneParser] a {PosixTimeZoneParser}
22
+ # instance to use to parse POSIX-style TZ strings.
23
+ # @param string_deduper [StringDeduper] a {StringDeduper} instance to use
24
+ # to dedupe abbreviations.
25
+ def initialize(posix_tz_parser, string_deduper)
26
+ @posix_tz_parser = posix_tz_parser
27
+ @string_deduper = string_deduper
28
+ end
29
+
30
+ # Reads a zoneinfo structure from the given path. Returns either a
31
+ # {TimezoneOffset} that is constantly observed or an `Array`
32
+ # {TimezoneTransition}s.
33
+ #
34
+ # @param file_path [String] the path of a zoneinfo file.
35
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
36
+ # {TimezoneTransition}s.
37
+ # @raise [SecurityError] if safe mode is enabled and `file_path` is
38
+ # tainted.
39
+ # @raise [InvalidZoneinfoFile] if `file_path`` does not refer to a valid
40
+ # zoneinfo file.
41
+ def read(file_path)
42
+ File.open(file_path, 'rb') { |file| parse(file) }
43
+ end
44
+
45
+ private
46
+
47
+ # Translates an unsigned 32-bit integer (as returned by unpack) to signed
48
+ # 32-bit.
49
+ #
50
+ # @param long [Integer] an unsigned 32-bit integer.
51
+ # @return [Integer] {long} translated to signed 32-bit.
52
+ def make_signed_int32(long)
53
+ long >= 0x80000000 ? long - 0x100000000 : long
54
+ end
55
+
56
+ # Translates a pair of unsigned 32-bit integers (as returned by unpack,
57
+ # most significant first) to a signed 64-bit integer.
58
+ #
59
+ # @param high [Integer] the most significant 32-bits.
60
+ # @param low [Integer] the least significant 32-bits.
61
+ # @return [Integer] {high} and {low} combined and translated to signed
62
+ # 64-bit.
63
+ def make_signed_int64(high, low)
64
+ unsigned = (high << 32) | low
65
+ unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
66
+ end
67
+
68
+ # Reads the given number of bytes from the given file and checks that the
69
+ # correct number of bytes could be read.
70
+ #
71
+ # @param file [IO] the file to read from.
72
+ # @param bytes [Integer] the number of bytes to read.
73
+ # @return [String] the bytes that were read.
74
+ # @raise [InvalidZoneinfoFile] if the number of bytes available didn't
75
+ # match the number requested.
76
+ def check_read(file, bytes)
77
+ result = file.read(bytes)
78
+
79
+ unless result && result.length == bytes
80
+ raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ # Zoneinfo files don't include the offset from standard time (std_offset)
87
+ # for DST periods. Derive the base offset (base_utc_offset) where DST is
88
+ # observed from either the previous or next non-DST period.
89
+ #
90
+ # @param transitions [Array<Hash>] an `Array` of transition hashes.
91
+ # @param offsets [Array<Hash>] an `Array` of offset hashes.
92
+ # @return [Integer] the index of the offset to be used prior to the first
93
+ # transition.
94
+ def derive_offsets(transitions, offsets)
95
+ # The first non-DST offset (if there is one) is the offset observed
96
+ # before the first transition. Fall back to the first DST offset if
97
+ # there are no non-DST offsets.
98
+ first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
99
+ first_offset_index = first_non_dst_offset_index || 0
100
+ return first_offset_index if transitions.empty?
101
+
102
+ # Determine the base_utc_offset of the next non-dst offset at each transition.
103
+ base_utc_offset_from_next = nil
104
+
105
+ transitions.reverse_each do |transition|
106
+ offset = offsets[transition[:offset]]
107
+ if offset[:is_dst]
108
+ transition[:base_utc_offset_from_next] = base_utc_offset_from_next if base_utc_offset_from_next
109
+ else
110
+ base_utc_offset_from_next = offset[:observed_utc_offset]
111
+ end
112
+ end
113
+
114
+ base_utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:observed_utc_offset] : nil
115
+ defined_offsets = {}
116
+
117
+ transitions.each do |transition|
118
+ offset_index = transition[:offset]
119
+ offset = offsets[offset_index]
120
+ observed_utc_offset = offset[:observed_utc_offset]
121
+
122
+ if offset[:is_dst]
123
+ base_utc_offset_from_next = transition[:base_utc_offset_from_next]
124
+
125
+ difference_to_previous = (observed_utc_offset - (base_utc_offset_from_previous || observed_utc_offset)).abs
126
+ difference_to_next = (observed_utc_offset - (base_utc_offset_from_next || observed_utc_offset)).abs
127
+
128
+ base_utc_offset = if difference_to_previous == 3600
129
+ base_utc_offset_from_previous
130
+ elsif difference_to_next == 3600
131
+ base_utc_offset_from_next
132
+ elsif difference_to_previous > 0 && difference_to_next > 0
133
+ difference_to_previous < difference_to_next ? base_utc_offset_from_previous : base_utc_offset_from_next
134
+ elsif difference_to_previous > 0
135
+ base_utc_offset_from_previous
136
+ elsif difference_to_next > 0
137
+ base_utc_offset_from_next
138
+ else
139
+ # No difference, assume a 1 hour offset from standard time.
140
+ observed_utc_offset - 3600
141
+ end
142
+
143
+ if !offset[:base_utc_offset]
144
+ offset[:base_utc_offset] = base_utc_offset
145
+ defined_offsets[offset] = offset_index
146
+ elsif offset[:base_utc_offset] != base_utc_offset
147
+ # An earlier transition has already derived a different
148
+ # base_utc_offset. Define a new offset or reuse an existing identically
149
+ # defined offset.
150
+ new_offset = offset.dup
151
+ new_offset[:base_utc_offset] = base_utc_offset
152
+
153
+ offset_index = defined_offsets[new_offset]
154
+
155
+ unless offset_index
156
+ offsets << new_offset
157
+ offset_index = offsets.length - 1
158
+ defined_offsets[new_offset] = offset_index
159
+ end
160
+
161
+ transition[:offset] = offset_index
162
+ end
163
+ else
164
+ base_utc_offset_from_previous = observed_utc_offset
165
+ end
166
+ end
167
+
168
+ first_offset_index
169
+ end
170
+
171
+ # Determines if the offset from a transition matches the offset from a
172
+ # rule. This is a looser match than equality, not requiring that the
173
+ # base_utc_offset and std_offset both match (which have to be derived for
174
+ # transitions, but are known for rules.
175
+ #
176
+ # @param offset [TimezoneOffset] an offset from a transition.
177
+ # @param rule_offset [TimezoneOffset] an offset from a rule.
178
+ # @return [Boolean] whether the offsets match.
179
+ def offset_matches_rule?(offset, rule_offset)
180
+ offset.observed_utc_offset == rule_offset.observed_utc_offset &&
181
+ offset.dst? == rule_offset.dst? &&
182
+ offset.abbreviation == rule_offset.abbreviation
183
+ end
184
+
185
+ # Apply the rules from the TZ string when there were no defined
186
+ # transitions. Checks for a matching offset. Returns the rules-based
187
+ # constant offset or generates transitions from 1970 until 100 years into
188
+ # the future (at the time of loading zoneinfo_reader.rb).
189
+ #
190
+ # @param file [IO] the file being processed.
191
+ # @param first_offset [TimezoneOffset] the first offset included in the
192
+ # file that would normally apply without the rules.
193
+ # @param rules [Object] a {TimezoneOffset} specifying a constant offset or
194
+ # {AnnualRules} instance specfying transitions.
195
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
196
+ # {TimezoneTransition}s.
197
+ # @raise [InvalidZoneinfoFile] if the first offset does not match the
198
+ # rules.
199
+ def apply_rules_without_transitions(file, first_offset, rules)
200
+ if rules.kind_of?(TimezoneOffset)
201
+ unless offset_matches_rule?(first_offset, rules)
202
+ raise InvalidZoneinfoFile, "Constant offset POSIX-style TZ string does not match constant offset in file '#{file.path}'."
203
+ end
204
+ rules
205
+ else
206
+ transitions = 1970.upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
207
+ first_transition = transitions[0]
208
+
209
+ unless offset_matches_rule?(first_offset, first_transition.previous_offset)
210
+ # Not transitioning from the designated first offset.
211
+
212
+ if offset_matches_rule?(first_offset, first_transition.offset)
213
+ # Skip an unnecessary transition to the first offset.
214
+ transitions.shift
215
+ else
216
+ # The initial offset doesn't match the ongoing rules. Replace the
217
+ # previous offset of the first transition.
218
+ transitions[0] = TimezoneTransition.new(first_transition.offset, first_offset, first_transition.timestamp_value)
219
+ end
220
+ end
221
+
222
+ transitions
223
+ end
224
+ end
225
+
226
+ # Finds an offset that is equivalent to the one specified in the given
227
+ # `Array`. Matching is performed with {TimezoneOffset#==}.
228
+ #
229
+ # @param offsets [Array<TimezoneOffset>] an `Array` to search.
230
+ # @param offset [TimezoneOffset] the offset to search for.
231
+ # @return [TimezoneOffset] the matching offset from `offsets` or `nil`
232
+ # if not found.
233
+ def find_existing_offset(offsets, offset)
234
+ offsets.find {|o| o == offset }
235
+ end
236
+
237
+ # Returns a new AnnualRules instance with standard and daylight savings
238
+ # offsets replaced with equivalents from an array. This reduces the memory
239
+ # requirement for loaded time zones by reusing offsets for rule-generated
240
+ # transitions.
241
+ #
242
+ # @param offsets [Array<TimezoneOffset>] an `Array` to search for
243
+ # equivalent offsets.
244
+ # @param annual_rules [AnnualRules] the {AnnualRules} instance to check.
245
+ # @return [AnnualRules] either a new {AnnualRules} instance with either
246
+ # the {AnnualRules#std_offset std_offset} or {AnnualRules#dst_offset
247
+ # dst_offset} replaced, or the original instance if no equivalent for
248
+ # either {AnnualRules#std_offset std_offset} or {AnnualRules#dst_offset
249
+ # dst_offset} could be found.
250
+ def replace_with_existing_offsets(offsets, annual_rules)
251
+ existing_std_offset = find_existing_offset(offsets, annual_rules.std_offset)
252
+ existing_dst_offset = find_existing_offset(offsets, annual_rules.dst_offset)
253
+ if existing_std_offset || existing_dst_offset
254
+ AnnualRules.new(existing_std_offset || annual_rules.std_offset, existing_dst_offset || annual_rules.dst_offset,
255
+ annual_rules.dst_start_rule, annual_rules.dst_end_rule)
256
+ else
257
+ annual_rules
258
+ end
259
+ end
260
+
261
+ # Validates the offset indicated to be observed by the rules before the
262
+ # first generated transition against the offset of the last defined
263
+ # transition.
264
+ #
265
+ # Fix the last defined transition if it differ on just base/std offsets
266
+ # (which are derived). Raise an error if the observed UTC offset or
267
+ # abbreviations differ.
268
+ #
269
+ # @param file [IO] the file being processed.
270
+ # @param last_defined [TimezoneTransition] the last defined transition in
271
+ # the file.
272
+ # @param first_rule_offset [TimezoneOffset] the offset the rules indicate
273
+ # is observed prior to the first rules generated transition.
274
+ # @return [TimezoneTransition] the last defined transition (either the
275
+ # original instance or a replacement).
276
+ # @raise [InvalidZoneinfoFile] if the offset of {last_defined} and
277
+ # {first_rule_offset} do not match.
278
+ def validate_and_fix_last_defined_transition_offset(file, last_defined, first_rule_offset)
279
+ offset_of_last_defined = last_defined.offset
280
+
281
+ if offset_of_last_defined == first_rule_offset
282
+ last_defined
283
+ else
284
+ if offset_matches_rule?(offset_of_last_defined, first_rule_offset)
285
+ # The same overall offset, but differing in the base or std
286
+ # offset (which are derived). Correct by using the rule.
287
+ TimezoneTransition.new(first_rule_offset, last_defined.previous_offset, last_defined.timestamp_value)
288
+ else
289
+ raise InvalidZoneinfoFile, "The first offset indicated by the POSIX-style TZ string did not match the final defined offset in file '#{file.path}'."
290
+ end
291
+ end
292
+ end
293
+
294
+ # Apply the rules from the TZ string when there were defined
295
+ # transitions. Checks for a matching offset with the last transition.
296
+ # Redefines the last transition if required and if the rules don't
297
+ # specific a constant offset, generates transitions until 100 years into
298
+ # the future (at the time of loading zoneinfo_reader.rb).
299
+ #
300
+ # @param file [IO] the file being processed.
301
+ # @param transitions [Array<TimezoneTransition>] the defined transitions.
302
+ # @param offsets [Array<TimezoneOffset>] the offsets used by the defined
303
+ # transitions.
304
+ # @param rules [Object] a {TimezoneOffset} specifying a constant offset or
305
+ # {AnnualRules} instance specfying transitions.
306
+ # @raise [InvalidZoneinfoFile] if the first offset does not match the
307
+ # rules.
308
+ # @raise [InvalidZoneinfoFile] if the previous offset of the first
309
+ # generated transition does not match the offset of the last defined
310
+ # transition.
311
+ def apply_rules_with_transitions(file, transitions, offsets, rules)
312
+ last_defined = transitions[-1]
313
+
314
+ if rules.kind_of?(TimezoneOffset)
315
+ transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, rules)
316
+ else
317
+ last_year = last_defined.local_end_at.to_time.year
318
+
319
+ if last_year <= GENERATE_UP_TO
320
+ rules = replace_with_existing_offsets(offsets, rules)
321
+
322
+ generated = rules.transitions(last_year).find_all do |t|
323
+ t.timestamp_value > last_defined.timestamp_value && !offset_matches_rule?(last_defined.offset, t.offset)
324
+ end
325
+
326
+ generated += (last_year + 1).upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
327
+
328
+ unless generated.empty?
329
+ transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, generated[0].previous_offset)
330
+ transitions.concat(generated)
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ # Parses a zoneinfo file and returns either a {TimezoneOffset} that is
337
+ # constantly observed or an `Array` of {TimezoneTransition}s.
338
+ #
339
+ # @param file [IO] the file to be read.
340
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
341
+ # {TimezoneTransition}s.
342
+ # @raise [InvalidZoneinfoFile] if the file is not a valid zoneinfo file.
343
+ def parse(file)
344
+ magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
345
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
346
+
347
+ if magic != 'TZif'
348
+ raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
349
+ end
350
+
351
+ if version == '2' || version == '3'
352
+ # Skip the first 32-bit section and read the header of the second 64-bit section
353
+ file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
354
+
355
+ prev_version = version
356
+
357
+ magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
358
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
359
+
360
+ unless magic == 'TZif' && (version == prev_version)
361
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
362
+ end
363
+
364
+ using_64bit = true
365
+ elsif version != '3' && version != '2' && version != "\0"
366
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
367
+ else
368
+ using_64bit = false
369
+ end
370
+
371
+ unless leapcnt == 0
372
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
373
+ end
374
+
375
+ transitions = if using_64bit
376
+ timecnt.times.map do |i|
377
+ high, low = check_read(file, 8).unpack('NN'.freeze)
378
+ transition_time = make_signed_int64(high, low)
379
+ {at: transition_time}
380
+ end
381
+ else
382
+ timecnt.times.map do |i|
383
+ transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
384
+ {at: transition_time}
385
+ end
386
+ end
387
+
388
+ check_read(file, timecnt).unpack('C*'.freeze).each_with_index do |localtime_type, i|
389
+ raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'." if localtime_type >= typecnt
390
+ transitions[i][:offset] = localtime_type
391
+ end
392
+
393
+ offsets = typecnt.times.map do |i|
394
+ gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
395
+ gmtoff = make_signed_int32(gmtoff)
396
+ isdst = isdst == 1
397
+ {observed_utc_offset: gmtoff, is_dst: isdst, abbr_index: abbrind}
398
+ end
399
+
400
+ abbrev = check_read(file, charcnt)
401
+
402
+ if using_64bit
403
+ # Skip to the POSIX-style TZ string.
404
+ file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
405
+ tz_string_start = check_read(file, 1)
406
+ raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
407
+ tz_string = file.readline("\n").force_encoding(Encoding::UTF_8)
408
+ raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
409
+
410
+ begin
411
+ rules = @posix_tz_parser.parse(tz_string)
412
+ rescue InvalidPosixTimeZone => e
413
+ raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
414
+ end
415
+ else
416
+ rules = nil
417
+ end
418
+
419
+ # Derive the offsets from standard time (std_offset).
420
+ first_offset_index = derive_offsets(transitions, offsets)
421
+
422
+ offsets = offsets.map do |o|
423
+ observed_utc_offset = o[:observed_utc_offset]
424
+ base_utc_offset = o[:base_utc_offset]
425
+
426
+ if base_utc_offset
427
+ # DST offset with base_utc_offset derived by derive_offsets.
428
+ std_offset = observed_utc_offset - base_utc_offset
429
+ elsif o[:is_dst]
430
+ # DST offset unreferenced by a transition (offset in use before the
431
+ # first transition). No derived base UTC offset, so assume 1 hour
432
+ # DST.
433
+ base_utc_offset = observed_utc_offset - 3600
434
+ std_offset = 3600
435
+ else
436
+ # Non-DST offset.
437
+ base_utc_offset = observed_utc_offset
438
+ std_offset = 0
439
+ end
440
+
441
+ abbrev_start = o[:abbr_index]
442
+ raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'." unless abbrev_start < abbrev.length
443
+
444
+ abbrev_end = abbrev.index("\0", abbrev_start)
445
+ raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'." unless abbrev_end
446
+
447
+ abbr = @string_deduper.dedupe(RubyCoreSupport.untaint(abbrev[abbrev_start...abbrev_end].force_encoding(Encoding::UTF_8)))
448
+
449
+ TimezoneOffset.new(base_utc_offset, std_offset, abbr)
450
+ end
451
+
452
+ first_offset = offsets[first_offset_index]
453
+
454
+
455
+ if transitions.empty?
456
+ if rules
457
+ apply_rules_without_transitions(file, first_offset, rules)
458
+ else
459
+ first_offset
460
+ end
461
+ else
462
+ previous_offset = first_offset
463
+ previous_at = nil
464
+
465
+ transitions = transitions.map do |t|
466
+ offset = offsets[t[:offset]]
467
+ at = t[:at]
468
+ 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
469
+ tt = TimezoneTransition.new(offset, previous_offset, at)
470
+ previous_offset = offset
471
+ previous_at = at
472
+ tt
473
+ end
474
+
475
+ apply_rules_with_transitions(file, transitions, offsets, rules) if rules
476
+ transitions
477
+ end
478
+ end
479
+ end
480
+ private_constant :ZoneinfoReader
481
+ end
482
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: UTF-8
2
+
3
+ module TZInfo
4
+ # {DataSource} implementations and classes used by {DataSource}
5
+ # implementations.
6
+ module DataSources
7
+ end
8
+ end
@@ -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
- # A Timezone based on a DataTimezoneInfo.
4
- #
5
- # @private
6
- class DataTimezone < InfoTimezone #:nodoc:
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
- # Returns the set of TimezonePeriod instances that are valid for the given
18
- # local time as an array. If you just want a single period, use
19
- # period_for_local instead and specify how abiguities should be resolved.
20
- # Raises PeriodNotFound if no periods are found for the given time.
21
- def periods_for_local(local)
22
- info.periods_for_local(local)
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 an Array of TimezoneTransition instances representing the times
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
- # If utc_from is specified and utc_to is not greater than utc_from, then
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
- # For a DataTimezone, this is always self.
39
+ # @return [Timezone] `self`.
54
40
  def canonical_zone
55
41
  self
56
42
  end