tzinfo 1.2.10 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.yardopts +3 -0
  4. data/CHANGES.md +573 -391
  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 +181 -0
  16. data/lib/tzinfo/data_sources/ruby_data_source.rb +145 -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 +596 -0
  20. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +486 -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/string_deduper.rb +118 -0
  42. data/lib/tzinfo/time_with_offset.rb +154 -0
  43. data/lib/tzinfo/timestamp.rb +552 -0
  44. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  45. data/lib/tzinfo/timezone.rb +989 -502
  46. data/lib/tzinfo/timezone_offset.rb +84 -74
  47. data/lib/tzinfo/timezone_period.rb +151 -217
  48. data/lib/tzinfo/timezone_proxy.rb +70 -79
  49. data/lib/tzinfo/timezone_transition.rb +77 -109
  50. data/lib/tzinfo/transition_rule.rb +207 -77
  51. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  52. data/lib/tzinfo/untaint_ext.rb +18 -0
  53. data/lib/tzinfo/version.rb +7 -0
  54. data/lib/tzinfo/with_offset.rb +61 -0
  55. data/lib/tzinfo.rb +82 -40
  56. data.tar.gz.sig +0 -0
  57. metadata +49 -104
  58. metadata.gz.sig +0 -0
  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_core_support.rb +0 -169
  67. data/lib/tzinfo/ruby_country_info.rb +0 -74
  68. data/lib/tzinfo/ruby_data_source.rb +0 -140
  69. data/lib/tzinfo/time_or_datetime.rb +0 -351
  70. data/lib/tzinfo/timezone_definition.rb +0 -36
  71. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  72. data/lib/tzinfo/timezone_info.rb +0 -30
  73. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  74. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  75. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  76. data/lib/tzinfo/zoneinfo_data_source.rb +0 -512
  77. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -520
  78. data/test/assets/payload.rb +0 -1
  79. data/test/tc_annual_rules.rb +0 -95
  80. data/test/tc_country.rb +0 -238
  81. data/test/tc_country_index_definition.rb +0 -69
  82. data/test/tc_country_info.rb +0 -16
  83. data/test/tc_country_timezone.rb +0 -173
  84. data/test/tc_data_source.rb +0 -218
  85. data/test/tc_data_timezone.rb +0 -99
  86. data/test/tc_data_timezone_info.rb +0 -18
  87. data/test/tc_info_timezone.rb +0 -34
  88. data/test/tc_linked_timezone.rb +0 -155
  89. data/test/tc_linked_timezone_info.rb +0 -23
  90. data/test/tc_offset_rationals.rb +0 -23
  91. data/test/tc_posix_time_zone_parser.rb +0 -261
  92. data/test/tc_ruby_core_support.rb +0 -168
  93. data/test/tc_ruby_country_info.rb +0 -110
  94. data/test/tc_ruby_data_source.rb +0 -173
  95. data/test/tc_time_or_datetime.rb +0 -674
  96. data/test/tc_timezone.rb +0 -1361
  97. data/test/tc_timezone_definition.rb +0 -113
  98. data/test/tc_timezone_index_definition.rb +0 -73
  99. data/test/tc_timezone_info.rb +0 -11
  100. data/test/tc_timezone_london.rb +0 -143
  101. data/test/tc_timezone_melbourne.rb +0 -142
  102. data/test/tc_timezone_new_york.rb +0 -142
  103. data/test/tc_timezone_offset.rb +0 -126
  104. data/test/tc_timezone_period.rb +0 -555
  105. data/test/tc_timezone_proxy.rb +0 -136
  106. data/test/tc_timezone_transition.rb +0 -366
  107. data/test/tc_timezone_transition_definition.rb +0 -295
  108. data/test/tc_timezone_utc.rb +0 -27
  109. data/test/tc_transition_data_timezone_info.rb +0 -433
  110. data/test/tc_transition_rule.rb +0 -663
  111. data/test/tc_zoneinfo_country_info.rb +0 -78
  112. data/test/tc_zoneinfo_data_source.rb +0 -1223
  113. data/test/tc_zoneinfo_timezone_info.rb +0 -2153
  114. data/test/test_utils.rb +0 -208
  115. data/test/ts_all.rb +0 -7
  116. data/test/ts_all_ruby.rb +0 -5
  117. data/test/ts_all_zoneinfo.rb +0 -9
  118. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  119. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -327
  120. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -230
  121. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  122. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  123. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  124. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  125. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -273
  126. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -198
  127. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -333
  128. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -277
  129. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -235
  130. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  131. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -940
  132. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -609
  133. data/test/tzinfo-data/tzinfo/data/version.rb +0 -20
  134. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  135. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  136. data/test/zoneinfo/America/New_York +0 -0
  137. data/test/zoneinfo/Australia/Melbourne +0 -0
  138. data/test/zoneinfo/EST +0 -0
  139. data/test/zoneinfo/Etc/UTC +0 -0
  140. data/test/zoneinfo/Europe/Amsterdam +0 -0
  141. data/test/zoneinfo/Europe/Andorra +0 -0
  142. data/test/zoneinfo/Europe/London +0 -0
  143. data/test/zoneinfo/Europe/Paris +0 -0
  144. data/test/zoneinfo/Europe/Prague +0 -0
  145. data/test/zoneinfo/Factory +0 -0
  146. data/test/zoneinfo/iso3166.tab +0 -274
  147. data/test/zoneinfo/leapseconds +0 -78
  148. data/test/zoneinfo/posix/Europe/London +0 -0
  149. data/test/zoneinfo/posixrules +0 -0
  150. data/test/zoneinfo/right/Europe/London +0 -0
  151. data/test/zoneinfo/zone.tab +0 -452
  152. data/test/zoneinfo/zone1970.tab +0 -384
  153. 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
@@ -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