tzinfo 1.2.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.yardopts +3 -0
  4. data/CHANGES.md +469 -431
  5. data/LICENSE +13 -13
  6. data/README.md +368 -114
  7. data/lib/tzinfo/country.rb +131 -129
  8. data/lib/tzinfo/country_timezone.rb +70 -112
  9. data/lib/tzinfo/data_source.rb +389 -144
  10. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  11. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  12. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  13. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  14. data/lib/tzinfo/data_sources/ruby_data_source.rb +141 -0
  15. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  16. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  17. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +573 -0
  18. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +284 -0
  19. data/lib/tzinfo/data_sources.rb +8 -0
  20. data/lib/tzinfo/data_timezone.rb +33 -47
  21. data/lib/tzinfo/datetime_with_offset.rb +153 -0
  22. data/lib/tzinfo/format1/country_definer.rb +17 -0
  23. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  24. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  25. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  26. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  27. data/lib/tzinfo/format1.rb +10 -0
  28. data/lib/tzinfo/format2/country_definer.rb +68 -0
  29. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  30. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  31. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  32. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  33. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  34. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
  35. data/lib/tzinfo/format2.rb +10 -0
  36. data/lib/tzinfo/info_timezone.rb +26 -21
  37. data/lib/tzinfo/linked_timezone.rb +33 -52
  38. data/lib/tzinfo/offset_timezone_period.rb +42 -0
  39. data/lib/tzinfo/string_deduper.rb +118 -0
  40. data/lib/tzinfo/time_with_offset.rb +128 -0
  41. data/lib/tzinfo/timestamp.rb +548 -0
  42. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  43. data/lib/tzinfo/timezone.rb +979 -502
  44. data/lib/tzinfo/timezone_offset.rb +84 -74
  45. data/lib/tzinfo/timezone_period.rb +151 -217
  46. data/lib/tzinfo/timezone_proxy.rb +70 -79
  47. data/lib/tzinfo/timezone_transition.rb +77 -109
  48. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  49. data/lib/tzinfo/version.rb +7 -0
  50. data/lib/tzinfo/with_offset.rb +61 -0
  51. data/lib/tzinfo.rb +60 -40
  52. data.tar.gz.sig +0 -0
  53. metadata +51 -115
  54. metadata.gz.sig +2 -3
  55. data/Rakefile +0 -107
  56. data/lib/tzinfo/annual_rules.rb +0 -51
  57. data/lib/tzinfo/country_index_definition.rb +0 -31
  58. data/lib/tzinfo/country_info.rb +0 -42
  59. data/lib/tzinfo/data_timezone_info.rb +0 -55
  60. data/lib/tzinfo/linked_timezone_info.rb +0 -26
  61. data/lib/tzinfo/offset_rationals.rb +0 -77
  62. data/lib/tzinfo/posix_time_zone_parser.rb +0 -136
  63. data/lib/tzinfo/ruby_core_support.rb +0 -176
  64. data/lib/tzinfo/ruby_country_info.rb +0 -74
  65. data/lib/tzinfo/ruby_data_source.rb +0 -136
  66. data/lib/tzinfo/time_or_datetime.rb +0 -351
  67. data/lib/tzinfo/timezone_definition.rb +0 -36
  68. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  69. data/lib/tzinfo/timezone_info.rb +0 -30
  70. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  71. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  72. data/lib/tzinfo/transition_rule.rb +0 -325
  73. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  74. data/lib/tzinfo/zoneinfo_data_source.rb +0 -504
  75. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -516
  76. data/test/assets/payload.rb +0 -1
  77. data/test/tc_annual_rules.rb +0 -95
  78. data/test/tc_country.rb +0 -240
  79. data/test/tc_country_index_definition.rb +0 -69
  80. data/test/tc_country_info.rb +0 -16
  81. data/test/tc_country_timezone.rb +0 -173
  82. data/test/tc_data_source.rb +0 -218
  83. data/test/tc_data_timezone.rb +0 -99
  84. data/test/tc_data_timezone_info.rb +0 -18
  85. data/test/tc_info_timezone.rb +0 -34
  86. data/test/tc_linked_timezone.rb +0 -155
  87. data/test/tc_linked_timezone_info.rb +0 -23
  88. data/test/tc_offset_rationals.rb +0 -23
  89. data/test/tc_posix_time_zone_parser.rb +0 -261
  90. data/test/tc_ruby_core_support.rb +0 -168
  91. data/test/tc_ruby_country_info.rb +0 -110
  92. data/test/tc_ruby_data_source.rb +0 -175
  93. data/test/tc_time_or_datetime.rb +0 -674
  94. data/test/tc_timezone.rb +0 -1361
  95. data/test/tc_timezone_definition.rb +0 -113
  96. data/test/tc_timezone_index_definition.rb +0 -73
  97. data/test/tc_timezone_info.rb +0 -11
  98. data/test/tc_timezone_london.rb +0 -143
  99. data/test/tc_timezone_melbourne.rb +0 -142
  100. data/test/tc_timezone_new_york.rb +0 -142
  101. data/test/tc_timezone_offset.rb +0 -126
  102. data/test/tc_timezone_period.rb +0 -555
  103. data/test/tc_timezone_proxy.rb +0 -136
  104. data/test/tc_timezone_transition.rb +0 -366
  105. data/test/tc_timezone_transition_definition.rb +0 -295
  106. data/test/tc_timezone_utc.rb +0 -27
  107. data/test/tc_transition_data_timezone_info.rb +0 -433
  108. data/test/tc_transition_rule.rb +0 -663
  109. data/test/tc_zoneinfo_country_info.rb +0 -78
  110. data/test/tc_zoneinfo_data_source.rb +0 -1226
  111. data/test/tc_zoneinfo_timezone_info.rb +0 -2149
  112. data/test/test_utils.rb +0 -214
  113. data/test/ts_all.rb +0 -7
  114. data/test/ts_all_ruby.rb +0 -5
  115. data/test/ts_all_zoneinfo.rb +0 -9
  116. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  117. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -327
  118. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -230
  119. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  120. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  121. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  122. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  123. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -273
  124. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -198
  125. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -333
  126. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -277
  127. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -235
  128. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  129. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -940
  130. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -609
  131. data/test/tzinfo-data/tzinfo/data/version.rb +0 -20
  132. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  133. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  134. data/test/zoneinfo/America/New_York +0 -0
  135. data/test/zoneinfo/Australia/Melbourne +0 -0
  136. data/test/zoneinfo/EST +0 -0
  137. data/test/zoneinfo/Etc/UTC +0 -0
  138. data/test/zoneinfo/Europe/Amsterdam +0 -0
  139. data/test/zoneinfo/Europe/Andorra +0 -0
  140. data/test/zoneinfo/Europe/London +0 -0
  141. data/test/zoneinfo/Europe/Paris +0 -0
  142. data/test/zoneinfo/Europe/Prague +0 -0
  143. data/test/zoneinfo/Factory +0 -0
  144. data/test/zoneinfo/iso3166.tab +0 -274
  145. data/test/zoneinfo/leapseconds +0 -78
  146. data/test/zoneinfo/posix/Europe/London +0 -0
  147. data/test/zoneinfo/posixrules +0 -0
  148. data/test/zoneinfo/right/Europe/London +0 -0
  149. data/test/zoneinfo/zone.tab +0 -452
  150. data/test/zoneinfo/zone1970.tab +0 -384
  151. data/tzinfo.gemspec +0 -21
@@ -1,516 +0,0 @@
1
- module TZInfo
2
- # An InvalidZoneinfoFile exception is raised if an attempt is made to load an
3
- # invalid zoneinfo file.
4
- class InvalidZoneinfoFile < StandardError
5
- end
6
-
7
- # Represents a timezone defined by a compiled zoneinfo TZif (\0, 2 or 3) file.
8
- #
9
- # @private
10
- class ZoneinfoTimezoneInfo < TransitionDataTimezoneInfo #:nodoc:
11
- # The year to generate transitions up to.
12
- #
13
- # @private
14
- GENERATE_UP_TO = RubyCoreSupport.time_supports_64bit ? Time.now.utc.year + 100 : 2037
15
-
16
- # Minimum supported timestamp (inclusive).
17
- #
18
- # Time.utc(1700, 1, 1).to_i
19
- MIN_TIMESTAMP = -8520336000
20
-
21
- # Maximum supported timestamp (exclusive).
22
- #
23
- # Time.utc(2500, 1, 1).to_i
24
- MAX_TIMESTAMP = 16725225600
25
-
26
- # Constructs the new ZoneinfoTimezoneInfo with an identifier, path
27
- # to the file and parser to use to parse the POSIX-like TZ string.
28
- def initialize(identifier, file_path, posix_tz_parser)
29
- super(identifier)
30
-
31
- File.open(file_path, 'rb') do |file|
32
- parse(file, posix_tz_parser)
33
- end
34
- end
35
-
36
- private
37
- # Unpack will return unsigned 32-bit integers. Translate to
38
- # signed 32-bit.
39
- def make_signed_int32(long)
40
- long >= 0x80000000 ? long - 0x100000000 : long
41
- end
42
-
43
- # Unpack will return a 64-bit integer as two unsigned 32-bit integers
44
- # (most significant first). Translate to signed 64-bit
45
- def make_signed_int64(high, low)
46
- unsigned = (high << 32) | low
47
- unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
48
- end
49
-
50
- # Read bytes from file and check that the correct number of bytes could
51
- # be read. Raises InvalidZoneinfoFile if the number of bytes didn't match
52
- # the number requested.
53
- def check_read(file, bytes)
54
- result = file.read(bytes)
55
-
56
- unless result && result.length == bytes
57
- raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
58
- end
59
-
60
- result
61
- end
62
-
63
- # Zoneinfo files don't include the offset from standard time (std_offset)
64
- # for DST periods. Derive the base offset (utc_offset) where DST is
65
- # observed from either the previous or next non-DST period.
66
- #
67
- # Returns the index of the offset to be used prior to the first
68
- # transition.
69
- def derive_offsets(transitions, offsets)
70
- # The first non-DST offset (if there is one) is the offset observed
71
- # before the first transition. Fallback to the first DST offset if there
72
- # are no non-DST offsets.
73
- first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
74
- first_offset_index = first_non_dst_offset_index || 0
75
- return first_offset_index if transitions.empty?
76
-
77
- # Determine the utc_offset of the next non-dst offset at each transition.
78
- utc_offset_from_next = nil
79
-
80
- transitions.reverse_each do |transition|
81
- offset = offsets[transition[:offset]]
82
- if offset[:is_dst]
83
- transition[:utc_offset_from_next] = utc_offset_from_next if utc_offset_from_next
84
- else
85
- utc_offset_from_next = offset[:utc_total_offset]
86
- end
87
- end
88
-
89
- utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:utc_total_offset] : nil
90
- defined_offsets = {}
91
-
92
- transitions.each do |transition|
93
- offset_index = transition[:offset]
94
- offset = offsets[offset_index]
95
- utc_total_offset = offset[:utc_total_offset]
96
-
97
- if offset[:is_dst]
98
- utc_offset_from_next = transition[:utc_offset_from_next]
99
-
100
- difference_to_previous = (utc_total_offset - (utc_offset_from_previous || utc_total_offset)).abs
101
- difference_to_next = (utc_total_offset - (utc_offset_from_next || utc_total_offset)).abs
102
-
103
- utc_offset = if difference_to_previous == 3600
104
- utc_offset_from_previous
105
- elsif difference_to_next == 3600
106
- utc_offset_from_next
107
- elsif difference_to_previous > 0 && difference_to_next > 0
108
- difference_to_previous < difference_to_next ? utc_offset_from_previous : utc_offset_from_next
109
- elsif difference_to_previous > 0
110
- utc_offset_from_previous
111
- elsif difference_to_next > 0
112
- utc_offset_from_next
113
- else
114
- # No difference, assume a 1 hour offset from standard time.
115
- utc_total_offset - 3600
116
- end
117
-
118
- if !offset[:utc_offset]
119
- offset[:utc_offset] = utc_offset
120
- defined_offsets[offset] = offset_index
121
- elsif offset[:utc_offset] != utc_offset
122
- # An earlier transition has already derived a different
123
- # utc_offset. Define a new offset or reuse an existing identically
124
- # defined offset.
125
- new_offset = offset.dup
126
- new_offset[:utc_offset] = utc_offset
127
-
128
- offset_index = defined_offsets[new_offset]
129
-
130
- unless offset_index
131
- offsets << new_offset
132
- offset_index = offsets.length - 1
133
- defined_offsets[new_offset] = offset_index
134
- end
135
-
136
- transition[:offset] = offset_index
137
- end
138
- else
139
- utc_offset_from_previous = utc_total_offset
140
- end
141
- end
142
-
143
- first_offset_index
144
- end
145
-
146
- # Remove transitions before a minimum supported value. If there is not a
147
- # transition exactly on the minimum supported value move the latest from
148
- # before up to the minimum supported value.
149
- def remove_unsupported_negative_transitions(transitions, min_supported)
150
- result = transitions.drop_while {|t| t[:at] < min_supported }
151
- if result.empty? || (result[0][:at] > min_supported && result.length < transitions.length)
152
- last_before = transitions[-1 - result.length]
153
- last_before[:at] = min_supported
154
- [last_before] + result
155
- else
156
- result
157
- end
158
- end
159
-
160
- # Determines if the offset from a transition matches the offset from a
161
- # rule. This is a looser match than TimezoneOffset#==, not requiring that
162
- # the utc_offset and std_offset both match (which have to be derived for
163
- # transitions, but are known for rules.
164
- def offset_matches_rule?(offset, rule_offset)
165
- offset[:utc_total_offset] == rule_offset.utc_total_offset &&
166
- offset[:is_dst] == rule_offset.dst? &&
167
- offset[:abbr] == rule_offset.abbreviation.to_s
168
- end
169
-
170
- # Determins if the offset from a transition exactly matches the offset
171
- # from a rule.
172
- def offset_equals_rule?(offset, rule_offset)
173
- offset_matches_rule?(offset, rule_offset) &&
174
- (offset[:utc_offset] || (offset[:is_dst] ? offset[:utc_total_offset] - 3600 : offset[:utc_total_offset])) == rule_offset.utc_offset
175
- end
176
-
177
- # Finds an offset hash that is an exact match to the rule offset specified.
178
- def find_existing_offset_index(offsets, rule_offset)
179
- offsets.find_index {|o| offset_equals_rule?(o, rule_offset) }
180
- end
181
-
182
- # Gets an existing matching offset index or adds a new offset hash for a
183
- # rule offset.
184
- def get_rule_offset_index(offsets, offset)
185
- index = find_existing_offset_index(offsets, offset)
186
- unless index
187
- index = offsets.length
188
- offsets << {:utc_total_offset => offset.utc_total_offset, :utc_offset => offset.utc_offset, :is_dst => offset.dst?, :abbr => offset.abbreviation}
189
- end
190
- index
191
- end
192
-
193
- # Gets a hash mapping rule offsets to indexes in offsets, creating new
194
- # offset hashes if required.
195
- def get_rule_offset_indexes(offsets, annual_rules)
196
- {
197
- annual_rules.std_offset => get_rule_offset_index(offsets, annual_rules.std_offset),
198
- annual_rules.dst_offset => get_rule_offset_index(offsets, annual_rules.dst_offset)
199
- }
200
- end
201
-
202
- # Converts an array of rule transitions to hashes.
203
- def convert_transitions_to_hashes(offset_indexes, transitions)
204
- transitions.map {|t| {:at => t.at.to_i, :offset => offset_indexes[t.offset]} }
205
- end
206
-
207
- # Apply the rules from the TZ string when there were no defined
208
- # transitions. Checks for a matching offset. Returns the rules-based
209
- # constant offset or generates transitions from 1970 until 100 years into
210
- # the future (at the time of loading zoneinfo_timezone_info.rb) or 2037 if
211
- # limited to 32-bit Times.
212
- def apply_rules_without_transitions(file, offsets, first_offset_index, rules)
213
- first_offset = offsets[first_offset_index]
214
-
215
- if rules.kind_of?(TimezoneOffset)
216
- unless offset_matches_rule?(first_offset, rules)
217
- raise InvalidZoneinfoFile, "Constant offset POSIX-style TZ string does not match constant offset in file '#{file.path}'."
218
- end
219
-
220
- first_offset[:utc_offset] = rules.utc_offset
221
- []
222
- else
223
- transitions = 1970.upto(GENERATE_UP_TO).map {|y| rules.transitions(y) }.flatten
224
- first_transition = transitions[0]
225
-
226
- if offset_matches_rule?(first_offset, first_transition.previous_offset)
227
- # Correct the first offset if it isn't an exact match.
228
- first_offset[:utc_offset] = first_transition.previous_offset.utc_offset
229
- else
230
- # Not transitioning from the designated first offset.
231
- if offset_matches_rule?(first_offset, first_transition.offset)
232
- # Correct the first offset if it isn't an exact match.
233
- first_offset[:utc_offset] = first_transition.offset.utc_offset
234
-
235
- # Skip an unnecessary transition to the first offset.
236
- transitions.shift
237
- end
238
-
239
- # If the first offset doesn't match either the offset or previous
240
- # offset, then it will be retained.
241
- end
242
-
243
- offset_indexes = get_rule_offset_indexes(offsets, rules)
244
- convert_transitions_to_hashes(offset_indexes, transitions)
245
- end
246
- end
247
-
248
- # Validates the rules offset against the offset of the last defined
249
- # transition. Replaces the transition with an equivalent using the rules
250
- # offset if the rules give a different definition for the base offset.
251
- def replace_last_transition_offset_if_valid_and_needed(file, transitions, offsets)
252
- last_transition = transitions.last
253
- last_offset = offsets[last_transition[:offset]]
254
- rule_offset = yield last_offset
255
-
256
- unless offset_matches_rule?(last_offset, rule_offset)
257
- raise InvalidZoneinfoFile, "Offset from POSIX-style TZ string does not match final transition in file '#{file.path}'."
258
- end
259
-
260
- # The total_utc_offset and abbreviation must always be the same. The
261
- # base utc_offset and std_offset might differ. In which case the rule
262
- # should be used as it will be more precise.
263
- last_offset[:utc_offset] = rule_offset.utc_offset
264
- last_transition
265
- end
266
-
267
- # todo: port over validate_and_fix_last_defined_transition_offset
268
- # when fixing the previous offset will need to define a new one
269
-
270
- # Validates the offset indicated to be observed by the rules before the
271
- # first generated transition against the offset of the last defined
272
- # transition.
273
- #
274
- # Fix the last defined transition if it differ on just base/std offsets
275
- # (which are derived). Raise an error if the observed UTC offset or
276
- # abbreviations differ.
277
- def validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, first_rule_offset)
278
- offset_of_last_defined = offsets[last_defined[:offset]]
279
-
280
- if offset_equals_rule?(offset_of_last_defined, first_rule_offset)
281
- last_defined
282
- else
283
- if offset_matches_rule?(offset_of_last_defined, first_rule_offset)
284
- # The same overall offset, but differing in the base or std
285
- # offset (which are derived). Correct by using the rule.
286
-
287
- offset_index = get_rule_offset_index(offsets, first_rule_offset)
288
- {:at => last_defined[:at], :offset => offset_index}
289
- else
290
- raise InvalidZoneinfoFile, "The first offset indicated by the POSIX-style TZ string did not match the final defined offset in file '#{file.path}'."
291
- end
292
- end
293
- end
294
-
295
- # Apply the rules from the TZ string when there were defined transitions.
296
- # Checks for a matching offset with the last transition. Redefines the
297
- # last transition if required and if the rules don't specific a constant
298
- # offset, generates transitions until 100 years into the future (at the
299
- # time of loading zoneinfo_timezone_info.rb) or 2037 if limited to 32-bit
300
- # Times.
301
- def apply_rules_with_transitions(file, transitions, offsets, first_offset_index, rules)
302
- last_defined = transitions[-1]
303
-
304
- if rules.kind_of?(TimezoneOffset)
305
- transitions[-1] = validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, rules)
306
- else
307
- previous_offset_index = transitions.length > 1 ? transitions[-2][:offset] : first_offset_index
308
- previous_offset = offsets[previous_offset_index]
309
- last_year = (Time.at(last_defined[:at]).utc + previous_offset[:utc_total_offset]).year
310
-
311
- if last_year <= GENERATE_UP_TO
312
- last_defined_offset = offsets[last_defined[:offset]]
313
-
314
- generated = rules.transitions(last_year).find_all do |t|
315
- t.at > last_defined[:at] && !offset_matches_rule?(last_defined_offset, t.offset)
316
- end
317
-
318
- generated += (last_year + 1).upto(GENERATE_UP_TO).map {|y| rules.transitions(y) }.flatten
319
-
320
- unless generated.empty?
321
- transitions[-1] = validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, generated[0].previous_offset)
322
- rule_offset_indexes = get_rule_offset_indexes(offsets, rules)
323
- transitions.concat(convert_transitions_to_hashes(rule_offset_indexes, generated))
324
- end
325
- end
326
- end
327
- end
328
-
329
- # Defines an offset for the timezone based on the given index and offset
330
- # Hash.
331
- def define_offset(index, offset)
332
- utc_total_offset = offset[:utc_total_offset]
333
- utc_offset = offset[:utc_offset]
334
-
335
- if utc_offset
336
- # DST offset with base utc_offset derived by derive_offsets.
337
- std_offset = utc_total_offset - utc_offset
338
- elsif offset[:is_dst]
339
- # DST offset unreferenced by a transition (offset in use before the
340
- # first transition). No derived base UTC offset, so assume 1 hour
341
- # DST.
342
- utc_offset = utc_total_offset - 3600
343
- std_offset = 3600
344
- else
345
- # Non-DST offset.
346
- utc_offset = utc_total_offset
347
- std_offset = 0
348
- end
349
-
350
- offset index, utc_offset, std_offset, RubyCoreSupport.untaint(offset[:abbr]).to_sym
351
- end
352
-
353
- # Parses a zoneinfo file and intializes the DataTimezoneInfo structures.
354
- def parse(file, posix_tz_parser)
355
- magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
356
- check_read(file, 44).unpack('a4 a x15 NNNNNN')
357
-
358
- if magic != 'TZif'
359
- raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
360
- end
361
-
362
- if version == '2' || version == '3'
363
- # Skip the first 32-bit section and read the header of the second
364
- # 64-bit section. The 64-bit section is always used even if the
365
- # runtime platform doesn't support 64-bit timestamps. In "slim" format
366
- # zoneinfo files the 32-bit section will be empty.
367
- file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
368
-
369
- prev_version = version
370
-
371
- magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
372
- check_read(file, 44).unpack('a4 a x15 NNNNNN')
373
-
374
- unless magic == 'TZif' && (version == prev_version)
375
- raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
376
- end
377
-
378
- using_64bit = true
379
- elsif version != '3' && version != '2' && version != "\0"
380
- raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
381
- else
382
- using_64bit = false
383
- end
384
-
385
- unless leapcnt == 0
386
- raise InvalidZoneinfoFile, "The zoneinfo file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
387
- end
388
-
389
- transitions = []
390
-
391
- if using_64bit
392
- timecnt.times do |i|
393
- high, low = check_read(file, 8).unpack('NN'.freeze)
394
- transition_time = make_signed_int64(high, low)
395
- transitions << {:at => transition_time}
396
- end
397
- else
398
- timecnt.times do |i|
399
- transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
400
- transitions << {:at => transition_time}
401
- end
402
- end
403
-
404
- timecnt.times do |i|
405
- localtime_type = check_read(file, 1).unpack('C'.freeze)[0]
406
- transitions[i][:offset] = localtime_type
407
- end
408
-
409
- offsets = []
410
-
411
- typecnt.times do |i|
412
- gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
413
- gmtoff = make_signed_int32(gmtoff)
414
- isdst = isdst == 1
415
- offset = {:utc_total_offset => gmtoff, :is_dst => isdst, :abbr_index => abbrind}
416
-
417
- unless isdst
418
- offset[:utc_offset] = gmtoff
419
- end
420
-
421
- offsets << offset
422
- end
423
-
424
- abbrev = check_read(file, charcnt)
425
-
426
- if using_64bit
427
- # Skip to the POSIX-style TZ string.
428
- file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
429
- tz_string_start = check_read(file, 1)
430
- raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
431
- tz_string = RubyCoreSupport.force_encoding(file.readline("\n"), 'UTF-8')
432
- raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
433
-
434
- begin
435
- rules = posix_tz_parser.parse(tz_string)
436
- rescue InvalidPosixTimeZone => e
437
- raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
438
- end
439
- else
440
- rules = nil
441
- end
442
-
443
- offsets.each do |o|
444
- abbrev_start = o[:abbr_index]
445
- raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'" unless abbrev_start < abbrev.length
446
-
447
- abbrev_end = abbrev.index("\0", abbrev_start)
448
- raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'" unless abbrev_end
449
-
450
- o[:abbr] = RubyCoreSupport.force_encoding(abbrev[abbrev_start...abbrev_end], 'UTF-8')
451
- end
452
-
453
- transitions.each do |t|
454
- if t[:offset] < 0 || t[:offset] >= offsets.length
455
- raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'."
456
- end
457
- end
458
-
459
- # Derive the offsets from standard time (std_offset).
460
- first_offset_index = derive_offsets(transitions, offsets)
461
-
462
- # Filter out transitions that are not supported by Time on this
463
- # platform.
464
- unless transitions.empty?
465
- if !RubyCoreSupport.time_supports_negative
466
- transitions = remove_unsupported_negative_transitions(transitions, 0)
467
- elsif !RubyCoreSupport.time_supports_64bit
468
- transitions = remove_unsupported_negative_transitions(transitions, -2**31)
469
- else
470
- # Ignore transitions that occur outside of a defined window. The
471
- # transition index cannot handle a large range of transition times.
472
- #
473
- # This is primarily intended to ignore the far in the past
474
- # transition added in zic 2014c (at timestamp -2**63 in zic 2014c
475
- # and at the approximate time of the big bang from zic 2014d).
476
- #
477
- # Assumes MIN_TIMESTAMP is less than -2**31.
478
- transitions = remove_unsupported_negative_transitions(transitions, MIN_TIMESTAMP)
479
- end
480
-
481
- if !RubyCoreSupport.time_supports_64bit
482
- i = transitions.find_index {|t| t[:at] >= 2**31 }
483
- had_later_transition = !!i
484
- transitions = transitions.first(i) if i
485
- else
486
- had_later_transition = false
487
- end
488
- end
489
-
490
- if rules && !had_later_transition
491
- if transitions.empty?
492
- transitions = apply_rules_without_transitions(file, offsets, first_offset_index, rules)
493
- else
494
- apply_rules_with_transitions(file, transitions, offsets, first_offset_index, rules)
495
- end
496
- end
497
-
498
- define_offset(first_offset_index, offsets[first_offset_index])
499
-
500
- used_offset_indexes = transitions.map {|t| t[:offset] }.to_set
501
-
502
- offsets.each_with_index do |o, i|
503
- define_offset(i, o) if i != first_offset_index && used_offset_indexes.include?(i)
504
- end
505
-
506
- # Ignore transitions that occur outside of a defined window. The
507
- # transition index cannot handle a large range of transition times.
508
- transitions.each do |t|
509
- at = t[:at]
510
- break if at >= MAX_TIMESTAMP
511
- time = Time.at(at).utc
512
- transition time.year, time.mon, t[:offset], at
513
- end
514
- end
515
- end
516
- end
@@ -1 +0,0 @@
1
- raise 'This should never be executed'
@@ -1,95 +0,0 @@
1
- require File.join(File.expand_path(File.dirname(__FILE__)), 'test_utils')
2
-
3
- include TZInfo
4
-
5
- class TCAnnualRules < Minitest::Test
6
-
7
- def test_initialize
8
- std_offset = TimezoneOffset.new(0, 0, 'GMT')
9
- dst_offset = TimezoneOffset.new(0, 3600, 'BST')
10
- dst_start_rule = FakeAlwaysDateAdjustmentRule.new(3, 1)
11
- dst_end_rule = FakeAlwaysDateAdjustmentRule.new(10, 1)
12
-
13
- rules = AnnualRules.new(std_offset, dst_offset, dst_start_rule, dst_end_rule)
14
- assert_same(std_offset, rules.std_offset)
15
- assert_same(dst_offset, rules.dst_offset)
16
- assert_same(dst_start_rule, rules.dst_start_rule)
17
- assert_same(dst_end_rule, rules.dst_end_rule)
18
- end
19
-
20
- [2020, 2021].each do |year|
21
- define_method "test_transitions_for_dst_mid_year_#{year}" do
22
- std_offset = TimezoneOffset.new(3600, 0, 'TEST')
23
- dst_offset = TimezoneOffset.new(3600, 3600, 'TESTS')
24
-
25
- rules = AnnualRules.new(
26
- std_offset,
27
- dst_offset,
28
- FakeAlwaysDateAdjustmentRule.new(3, 21),
29
- FakeAlwaysDateAdjustmentRule.new(10, 22)
30
- )
31
-
32
- result = rules.transitions(year)
33
-
34
- expected = [
35
- AnnualRules::Transition.new(dst_offset, std_offset, TimeOrDateTime.wrap(Time.utc(year, 3, 21, 0, 0, 0) - 3600)),
36
- AnnualRules::Transition.new(std_offset, dst_offset, TimeOrDateTime.wrap(Time.utc(year, 10, 22, 0, 0, 0) - 7200))
37
- ]
38
-
39
- assert_equal(expected, result)
40
- end
41
-
42
- define_method "test_transitions_for_dst_start_and_end_of_year_#{year}" do
43
- std_offset = TimezoneOffset.new(3600, 0, 'TEST')
44
- dst_offset = TimezoneOffset.new(3600, 3600, 'TESTS')
45
-
46
- rules = AnnualRules.new(
47
- std_offset,
48
- dst_offset,
49
- FakeAlwaysDateAdjustmentRule.new(10, 22),
50
- FakeAlwaysDateAdjustmentRule.new(3, 21)
51
- )
52
-
53
- result = rules.transitions(year)
54
-
55
- expected = [
56
- AnnualRules::Transition.new(std_offset, dst_offset, TimeOrDateTime.wrap(Time.utc(year, 3, 21, 0, 0, 0) - 7200)),
57
- AnnualRules::Transition.new(dst_offset, std_offset, TimeOrDateTime.wrap(Time.utc(year, 10, 22, 0, 0, 0) - 3600))
58
- ]
59
-
60
- assert_equal(expected, result)
61
- end
62
-
63
- define_method "test_transitions_for_negative_dst_start_and_end_of_year_#{year}" do
64
- std_offset = TimezoneOffset.new(7200, 0, 'TEST')
65
- dst_offset = TimezoneOffset.new(7200, -3600, 'TESTW')
66
-
67
- rules = AnnualRules.new(
68
- std_offset,
69
- dst_offset,
70
- FakeAlwaysDateAdjustmentRule.new(10, 22),
71
- FakeAlwaysDateAdjustmentRule.new(3, 21)
72
- )
73
-
74
- result = rules.transitions(year)
75
-
76
- expected = [
77
- AnnualRules::Transition.new(std_offset, dst_offset, TimeOrDateTime.wrap(Time.utc(year, 3, 21, 0, 0, 0) - 3600)),
78
- AnnualRules::Transition.new(dst_offset, std_offset, TimeOrDateTime.wrap(Time.utc(year, 10, 22, 0, 0, 0) - 7200))
79
- ]
80
-
81
- assert_equal(expected, result)
82
- end
83
- end
84
-
85
- class FakeAlwaysDateAdjustmentRule
86
- def initialize(month, day)
87
- @month = month
88
- @day = day
89
- end
90
-
91
- def at(offset, year)
92
- TimeOrDateTime.wrap(Time.utc(year, @month, @day, 0, 0, 0) - offset.utc_total_offset)
93
- end
94
- end
95
- end