tzinfo 1.2.5 → 1.2.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of tzinfo might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -3
  3. data.tar.gz.sig +0 -0
  4. data/CHANGES.md +86 -48
  5. data/LICENSE +1 -1
  6. data/README.md +9 -8
  7. data/lib/tzinfo.rb +3 -0
  8. data/lib/tzinfo/annual_rules.rb +51 -0
  9. data/lib/tzinfo/data_source.rb +1 -1
  10. data/lib/tzinfo/posix_time_zone_parser.rb +136 -0
  11. data/lib/tzinfo/ruby_core_support.rb +24 -1
  12. data/lib/tzinfo/ruby_data_source.rb +24 -20
  13. data/lib/tzinfo/time_or_datetime.rb +11 -0
  14. data/lib/tzinfo/timezone.rb +10 -6
  15. data/lib/tzinfo/transition_rule.rb +325 -0
  16. data/lib/tzinfo/zoneinfo_data_source.rb +10 -1
  17. data/lib/tzinfo/zoneinfo_timezone_info.rb +264 -40
  18. data/test/tc_annual_rules.rb +95 -0
  19. data/test/tc_country.rb +6 -2
  20. data/test/tc_posix_time_zone_parser.rb +261 -0
  21. data/test/tc_ruby_data_source.rb +26 -2
  22. data/test/tc_time_or_datetime.rb +26 -6
  23. data/test/tc_timezone.rb +13 -2
  24. data/test/tc_transition_data_timezone_info.rb +11 -1
  25. data/test/tc_transition_rule.rb +663 -0
  26. data/test/tc_zoneinfo_data_source.rb +11 -2
  27. data/test/tc_zoneinfo_timezone_info.rb +1034 -113
  28. data/test/test_utils.rb +32 -3
  29. data/test/ts_all_zoneinfo.rb +3 -1
  30. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +5 -5
  31. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +13 -1
  32. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +13 -1
  33. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +1 -1
  34. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +2 -2
  35. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +2 -2
  36. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +1 -1
  37. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +15 -3
  38. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +13 -1
  39. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +13 -1
  40. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +15 -3
  41. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +19 -4
  42. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +1 -1
  43. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +197 -184
  44. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +60 -47
  45. data/test/tzinfo-data/tzinfo/data/version.rb +9 -3
  46. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  47. data/test/zoneinfo/America/New_York +0 -0
  48. data/test/zoneinfo/Australia/Melbourne +0 -0
  49. data/test/zoneinfo/EST +0 -0
  50. data/test/zoneinfo/Etc/UTC +0 -0
  51. data/test/zoneinfo/Europe/Amsterdam +0 -0
  52. data/test/zoneinfo/Europe/Andorra +0 -0
  53. data/test/zoneinfo/Europe/London +0 -0
  54. data/test/zoneinfo/Europe/Paris +0 -0
  55. data/test/zoneinfo/Europe/Prague +0 -0
  56. data/test/zoneinfo/Factory +0 -0
  57. data/test/zoneinfo/iso3166.tab +13 -14
  58. data/test/zoneinfo/leapseconds +38 -21
  59. data/test/zoneinfo/posix/Europe/London +0 -0
  60. data/test/zoneinfo/posixrules +0 -0
  61. data/test/zoneinfo/right/Europe/London +0 -0
  62. data/test/zoneinfo/zone.tab +172 -159
  63. data/test/zoneinfo/zone1970.tab +185 -170
  64. data/tzinfo.gemspec +2 -2
  65. metadata +28 -24
  66. metadata.gz.sig +0 -0
@@ -0,0 +1,325 @@
1
+ require 'date'
2
+
3
+ module TZInfo
4
+ # Base class for rules definining the transition between standard and daylight
5
+ # savings time.
6
+ class TransitionRule #:nodoc:
7
+ # Returns the number of seconds after midnight local time on the day
8
+ # identified by the rule at which the transition occurs. Can be negative to
9
+ # denote a time on the prior day. Can be greater than or equal to 86,400 to
10
+ # denote a time of the following day.
11
+ attr_reader :transition_at
12
+
13
+ # Initializes a new TransitionRule.
14
+ def initialize(transition_at)
15
+ raise ArgumentError, 'Invalid transition_at' unless transition_at.kind_of?(Integer)
16
+ @transition_at = transition_at
17
+ end
18
+
19
+ # Calculates the UTC time of the transition from a given offset on a given
20
+ # year.
21
+ def at(offset, year)
22
+ day = get_day(year)
23
+ day.add_with_convert(@transition_at - offset.utc_total_offset)
24
+ end
25
+
26
+ # Determines if this TransitionRule is equal to another instance.
27
+ def ==(r)
28
+ r.kind_of?(TransitionRule) && @transition_at == r.transition_at
29
+ end
30
+ alias eql? ==
31
+
32
+ # Returns a hash based on hash_args (defaulting to transition_at).
33
+ def hash
34
+ hash_args.hash
35
+ end
36
+
37
+ protected
38
+
39
+ # Returns an Array of parameters that will influence the output of hash.
40
+ def hash_args
41
+ [@transition_at]
42
+ end
43
+
44
+ def new_time_or_datetime(year, month = 1, day = 1)
45
+ result = if ((year >= 2039 || (year == 2038 && (month >= 2 || (month == 1 && day >= 20)))) && !RubyCoreSupport.time_supports_64bit) ||
46
+ (year < 1970 && !RubyCoreSupport.time_supports_negative)
47
+
48
+ # Time handles 29 February on a non-leap year as 1 March.
49
+ # DateTime rejects. Advance manually.
50
+ if month == 2 && day == 29 && !Date.gregorian_leap?(year)
51
+ month = 3
52
+ day = 1
53
+ end
54
+
55
+ RubyCoreSupport.datetime_new(year, month, day)
56
+ else
57
+ Time.utc(year, month, day)
58
+ end
59
+
60
+ TimeOrDateTime.wrap(result)
61
+ end
62
+ end
63
+
64
+ # A base class for transition rules that activate based on an integer day of
65
+ # the year.
66
+ #
67
+ # @private
68
+ class DayOfYearTransitionRule < TransitionRule #:nodoc:
69
+ # Initializes a new DayOfYearTransitionRule.
70
+ def initialize(day, transition_at)
71
+ super(transition_at)
72
+ raise ArgumentError, 'Invalid day' unless day.kind_of?(Integer)
73
+ @seconds = day * 86400
74
+ end
75
+
76
+ # Determines if this DayOfYearTransitionRule is equal to another instance.
77
+ def ==(r)
78
+ super(r) && r.kind_of?(DayOfYearTransitionRule) && @seconds == r.seconds
79
+ end
80
+ alias eql? ==
81
+
82
+ protected
83
+
84
+ # @return [Integer] the day multipled by the number of seconds in a day.
85
+ attr_reader :seconds
86
+
87
+ # Returns an Array of parameters that will influence the output of hash.
88
+ def hash_args
89
+ [@seconds] + super
90
+ end
91
+ end
92
+
93
+ # Defines transitions that occur on the zero-based nth day of the year.
94
+ #
95
+ # Day 0 is 1 January.
96
+ #
97
+ # Leap days are counted. Day 59 will be 29 February on a leap year and 1 March
98
+ # on a non-leap year. Day 365 will be 31 December on a leap year and 1 January
99
+ # the following year on a non-leap year.
100
+ #
101
+ # @private
102
+ class AbsoluteDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
103
+ # Initializes a new AbsoluteDayOfYearTransitionRule.
104
+ def initialize(day, transition_at = 0)
105
+ super(day, transition_at)
106
+ raise ArgumentError, 'Invalid day' unless day >= 0 && day <= 365
107
+ end
108
+
109
+ # Returns true if the day specified by this transition is the first in the
110
+ # year (a day number of 0), otherwise false.
111
+ def is_always_first_day_of_year?
112
+ seconds == 0
113
+ end
114
+
115
+ # @returns false.
116
+ def is_always_last_day_of_year?
117
+ false
118
+ end
119
+
120
+ # Determines if this AbsoluteDayOfYearTransitionRule is equal to another
121
+ # instance.
122
+ def ==(r)
123
+ super(r) && r.kind_of?(AbsoluteDayOfYearTransitionRule)
124
+ end
125
+ alias eql? ==
126
+
127
+ protected
128
+
129
+ # Returns a TimeOrDateTime representing midnight local time on the day
130
+ # specified by the rule for the given offset and year.
131
+ def get_day(year)
132
+ new_time_or_datetime(year).add_with_convert(seconds)
133
+ end
134
+
135
+ # Returns an Array of parameters that will influence the output of hash.
136
+ def hash_args
137
+ [AbsoluteDayOfYearTransitionRule] + super
138
+ end
139
+ end
140
+
141
+ # Defines transitions that occur on the one-based nth Julian day of the year.
142
+ #
143
+ # Leap days are not counted. Day 1 is 1 January. Day 60 is always 1 March.
144
+ # Day 365 is always 31 December.
145
+ #
146
+ # @private
147
+ class JulianDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
148
+ # The 60 days in seconds.
149
+ LEAP = 60 * 86400
150
+
151
+ # The length of a non-leap year in seconds.
152
+ YEAR = 365 * 86400
153
+
154
+ # Initializes a new JulianDayOfYearTransitionRule.
155
+ def initialize(day, transition_at = 0)
156
+ super(day, transition_at)
157
+ raise ArgumentError, 'Invalid day' unless day >= 1 && day <= 365
158
+ end
159
+
160
+ # Returns true if the day specified by this transition is the first in the
161
+ # year (a day number of 1), otherwise false.
162
+ def is_always_first_day_of_year?
163
+ seconds == 86400
164
+ end
165
+
166
+ # Returns true if the day specified by this transition is the last in the
167
+ # year (a day number of 365), otherwise false.
168
+ def is_always_last_day_of_year?
169
+ seconds == YEAR
170
+ end
171
+
172
+ # Determines if this JulianDayOfYearTransitionRule is equal to another
173
+ # instance.
174
+ def ==(r)
175
+ super(r) && r.kind_of?(JulianDayOfYearTransitionRule)
176
+ end
177
+ alias eql? ==
178
+
179
+ protected
180
+
181
+ # Returns a TimeOrDateTime representing midnight local time on the day
182
+ # specified by the rule for the given offset and year.
183
+ def get_day(year)
184
+ # Returns 1 March on non-leap years.
185
+ leap = new_time_or_datetime(year, 2, 29)
186
+ diff = seconds - LEAP
187
+ diff += 86400 if diff >= 0 && leap.mday == 29
188
+ leap.add_with_convert(diff)
189
+ end
190
+
191
+ # Returns an Array of parameters that will influence the output of hash.
192
+ def hash_args
193
+ [JulianDayOfYearTransitionRule] + super
194
+ end
195
+ end
196
+
197
+ # A base class for rules that transition on a particular day of week of a
198
+ # given week (subclasses specify which week of the month).
199
+ #
200
+ # @private
201
+ class DayOfWeekTransitionRule < TransitionRule #:nodoc:
202
+ # Initializes a new DayOfWeekTransitionRule.
203
+ def initialize(month, day_of_week, transition_at)
204
+ super(transition_at)
205
+ raise ArgumentError, 'Invalid month' unless month.kind_of?(Integer) && month >= 1 && month <= 12
206
+ raise ArgumentError, 'Invalid day_of_week' unless day_of_week.kind_of?(Integer) && day_of_week >= 0 && day_of_week <= 6
207
+ @month = month
208
+ @day_of_week = day_of_week
209
+ end
210
+
211
+ # Returns false.
212
+ def is_always_first_day_of_year?
213
+ false
214
+ end
215
+
216
+ # Returns false.
217
+ def is_always_last_day_of_year?
218
+ false
219
+ end
220
+
221
+ # Determines if this DayOfWeekTransitionRule is equal to another instance.
222
+ def ==(r)
223
+ super(r) && r.kind_of?(DayOfWeekTransitionRule) && @month == r.month && @day_of_week == r.day_of_week
224
+ end
225
+ alias eql? ==
226
+
227
+ protected
228
+
229
+ # Returns the month of the year (1 to 12).
230
+ attr_reader :month
231
+
232
+ # Returns the day of the week (0 to 6 for Sunday to Monday).
233
+ attr_reader :day_of_week
234
+
235
+ # Returns an Array of parameters that will influence the output of hash.
236
+ def hash_args
237
+ [@month, @day_of_week] + super
238
+ end
239
+ end
240
+
241
+ # A rule that transitions on the nth occurrence of a particular day of week
242
+ # of a calendar month.
243
+ #
244
+ # @private
245
+ class DayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
246
+ # Initializes a new DayOfMonthTransitionRule.
247
+ def initialize(month, week, day_of_week, transition_at = 0)
248
+ super(month, day_of_week, transition_at)
249
+ raise ArgumentError, 'Invalid week' unless week.kind_of?(Integer) && week >= 1 && week <= 4
250
+ @offset_start = (week - 1) * 7 + 1
251
+ end
252
+
253
+ # Determines if this DayOfMonthTransitionRule is equal to another instance.
254
+ def ==(r)
255
+ super(r) && r.kind_of?(DayOfMonthTransitionRule) && @offset_start == r.offset_start
256
+ end
257
+ alias eql? ==
258
+
259
+ protected
260
+
261
+ # Returns the day the week starts on for a month starting on a Sunday.
262
+ attr_reader :offset_start
263
+
264
+ # Returns a TimeOrDateTime representing midnight local time on the day
265
+ # specified by the rule for the given offset and year.
266
+ def get_day(year)
267
+ candidate = new_time_or_datetime(year, month, @offset_start)
268
+ diff = day_of_week - candidate.wday
269
+
270
+ if diff < 0
271
+ candidate.add_with_convert((7 + diff) * 86400)
272
+ elsif diff > 0
273
+ candidate.add_with_convert(diff * 86400)
274
+ else
275
+ candidate
276
+ end
277
+ end
278
+
279
+ # Returns an Array of parameters that will influence the output of hash.
280
+ def hash_args
281
+ [@offset_start] + super
282
+ end
283
+ end
284
+
285
+ # A rule that transitions on the last occurrence of a particular day of week
286
+ # of a calendar month.
287
+ #
288
+ # @private
289
+ class LastDayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
290
+ # Initializes a new LastDayOfMonthTransitionRule.
291
+ def initialize(month, day_of_week, transition_at = 0)
292
+ super(month, day_of_week, transition_at)
293
+ end
294
+
295
+ # Determines if this LastDayOfMonthTransitionRule is equal to another
296
+ # instance.
297
+ def ==(r)
298
+ super(r) && r.kind_of?(LastDayOfMonthTransitionRule)
299
+ end
300
+ alias eql? ==
301
+
302
+ protected
303
+
304
+ # Returns a TimeOrDateTime representing midnight local time on the day
305
+ # specified by the rule for the given offset and year.
306
+ def get_day(year)
307
+ next_month = month + 1
308
+ if next_month == 13
309
+ year += 1
310
+ next_month = 1
311
+ end
312
+
313
+ candidate = new_time_or_datetime(year, next_month).add_with_convert(-86400)
314
+ diff = candidate.wday - day_of_week
315
+
316
+ if diff < 0
317
+ candidate - (diff + 7) * 86400
318
+ elsif diff > 0
319
+ candidate - diff * 86400
320
+ else
321
+ candidate
322
+ end
323
+ end
324
+ end
325
+ end
@@ -1,4 +1,12 @@
1
1
  module TZInfo
2
+ # Use send as a workaround for an issue on JRuby 9.2.9.0 where using the
3
+ # refinement causes calls to RubyCoreSupport.file_open to fail to pass the
4
+ # block parameter.
5
+ #
6
+ # https://travis-ci.org/tzinfo/tzinfo/jobs/628812051#L1931
7
+ # https://github.com/jruby/jruby/issues/6009
8
+ send(:using, TZInfo::RubyCoreSupport::UntaintExt) if TZInfo::RubyCoreSupport.const_defined?(:UntaintExt)
9
+
2
10
  # An InvalidZoneinfoDirectory exception is raised if the DataSource is
3
11
  # set to a specific zoneinfo path, which is not a valid zoneinfo directory
4
12
  # (i.e. a directory containing index files named iso3166.tab and zone.tab
@@ -184,6 +192,7 @@ module TZInfo
184
192
  @zoneinfo_dir = File.expand_path(@zoneinfo_dir).freeze
185
193
  @timezone_index = load_timezone_index.freeze
186
194
  @country_index = load_country_index(iso3166_tab_path, zone_tab_path).freeze
195
+ @posix_tz_parser = PosixTimeZoneParser.new
187
196
  end
188
197
 
189
198
  # Returns a TimezoneInfo instance for a given identifier.
@@ -200,7 +209,7 @@ module TZInfo
200
209
  path.untaint
201
210
 
202
211
  begin
203
- ZoneinfoTimezoneInfo.new(identifier, path)
212
+ ZoneinfoTimezoneInfo.new(identifier, path, @posix_tz_parser)
204
213
  rescue InvalidZoneinfoFile => e
205
214
  raise InvalidTimezoneIdentifier, e.message
206
215
  end
@@ -1,4 +1,8 @@
1
1
  module TZInfo
2
+ # Use send as a workaround for erroneous 'wrong number of arguments' errors
3
+ # with JRuby 9.0.5.0 when calling methods with Java implementations. See #114.
4
+ send(:using, RubyCoreSupport::UntaintExt) if RubyCoreSupport.const_defined?(:UntaintExt)
5
+
2
6
  # An InvalidZoneinfoFile exception is raised if an attempt is made to load an
3
7
  # invalid zoneinfo file.
4
8
  class InvalidZoneinfoFile < StandardError
@@ -8,7 +12,11 @@ module TZInfo
8
12
  #
9
13
  # @private
10
14
  class ZoneinfoTimezoneInfo < TransitionDataTimezoneInfo #:nodoc:
11
-
15
+ # The year to generate transitions up to.
16
+ #
17
+ # @private
18
+ GENERATE_UP_TO = RubyCoreSupport.time_supports_64bit ? Time.now.utc.year + 100 : 2037
19
+
12
20
  # Minimum supported timestamp (inclusive).
13
21
  #
14
22
  # Time.utc(1700, 1, 1).to_i
@@ -19,13 +27,13 @@ module TZInfo
19
27
  # Time.utc(2500, 1, 1).to_i
20
28
  MAX_TIMESTAMP = 16725225600
21
29
 
22
- # Constructs the new ZoneinfoTimezoneInfo with an identifier and path
23
- # to the file.
24
- def initialize(identifier, file_path)
30
+ # Constructs the new ZoneinfoTimezoneInfo with an identifier, path
31
+ # to the file and parser to use to parse the POSIX-like TZ string.
32
+ def initialize(identifier, file_path, posix_tz_parser)
25
33
  super(identifier)
26
34
 
27
35
  File.open(file_path, 'rb') do |file|
28
- parse(file)
36
+ parse(file, posix_tz_parser)
29
37
  end
30
38
  end
31
39
 
@@ -55,7 +63,7 @@ module TZInfo
55
63
 
56
64
  result
57
65
  end
58
-
66
+
59
67
  # Zoneinfo files don't include the offset from standard time (std_offset)
60
68
  # for DST periods. Derive the base offset (utc_offset) where DST is
61
69
  # observed from either the previous or next non-DST period.
@@ -139,6 +147,189 @@ module TZInfo
139
147
  first_offset_index
140
148
  end
141
149
 
150
+ # Remove transitions before a minimum supported value. If there is not a
151
+ # transition exactly on the minimum supported value move the latest from
152
+ # before up to the minimum supported value.
153
+ def remove_unsupported_negative_transitions(transitions, min_supported)
154
+ result = transitions.drop_while {|t| t[:at] < min_supported }
155
+ if result.empty? || (result[0][:at] > min_supported && result.length < transitions.length)
156
+ last_before = transitions[-1 - result.length]
157
+ last_before[:at] = min_supported
158
+ [last_before] + result
159
+ else
160
+ result
161
+ end
162
+ end
163
+
164
+ # Determines if the offset from a transition matches the offset from a
165
+ # rule. This is a looser match than TimezoneOffset#==, not requiring that
166
+ # the utc_offset and std_offset both match (which have to be derived for
167
+ # transitions, but are known for rules.
168
+ def offset_matches_rule?(offset, rule_offset)
169
+ offset[:utc_total_offset] == rule_offset.utc_total_offset &&
170
+ offset[:is_dst] == rule_offset.dst? &&
171
+ offset[:abbr] == rule_offset.abbreviation.to_s
172
+ end
173
+
174
+ # Determins if the offset from a transition exactly matches the offset
175
+ # from a rule.
176
+ def offset_equals_rule?(offset, rule_offset)
177
+ offset_matches_rule?(offset, rule_offset) &&
178
+ (offset[:utc_offset] || (offset[:is_dst] ? offset[:utc_total_offset] - 3600 : offset[:utc_total_offset])) == rule_offset.utc_offset
179
+ end
180
+
181
+ # Finds an offset hash that is an exact match to the rule offset specified.
182
+ def find_existing_offset_index(offsets, rule_offset)
183
+ offsets.find_index {|o| offset_equals_rule?(o, rule_offset) }
184
+ end
185
+
186
+ # Gets an existing matching offset index or adds a new offset hash for a
187
+ # rule offset.
188
+ def get_rule_offset_index(offsets, offset)
189
+ index = find_existing_offset_index(offsets, offset)
190
+ unless index
191
+ index = offsets.length
192
+ offsets << {:utc_total_offset => offset.utc_total_offset, :utc_offset => offset.utc_offset, :is_dst => offset.dst?, :abbr => offset.abbreviation}
193
+ end
194
+ index
195
+ end
196
+
197
+ # Gets a hash mapping rule offsets to indexes in offsets, creating new
198
+ # offset hashes if required.
199
+ def get_rule_offset_indexes(offsets, annual_rules)
200
+ {
201
+ annual_rules.std_offset => get_rule_offset_index(offsets, annual_rules.std_offset),
202
+ annual_rules.dst_offset => get_rule_offset_index(offsets, annual_rules.dst_offset)
203
+ }
204
+ end
205
+
206
+ # Converts an array of rule transitions to hashes.
207
+ def convert_transitions_to_hashes(offset_indexes, transitions)
208
+ transitions.map {|t| {:at => t.at.to_i, :offset => offset_indexes[t.offset]} }
209
+ end
210
+
211
+ # Apply the rules from the TZ string when there were no defined
212
+ # transitions. Checks for a matching offset. Returns the rules-based
213
+ # constant offset or generates transitions from 1970 until 100 years into
214
+ # the future (at the time of loading zoneinfo_timezone_info.rb) or 2037 if
215
+ # limited to 32-bit Times.
216
+ def apply_rules_without_transitions(file, offsets, first_offset_index, rules)
217
+ first_offset = offsets[first_offset_index]
218
+
219
+ if rules.kind_of?(TimezoneOffset)
220
+ unless offset_matches_rule?(first_offset, rules)
221
+ raise InvalidZoneinfoFile, "Constant offset POSIX-style TZ string does not match constant offset in file '#{file.path}'."
222
+ end
223
+
224
+ first_offset[:utc_offset] = rules.utc_offset
225
+ []
226
+ else
227
+ transitions = 1970.upto(GENERATE_UP_TO).map {|y| rules.transitions(y) }.flatten
228
+ first_transition = transitions[0]
229
+
230
+ if offset_matches_rule?(first_offset, first_transition.previous_offset)
231
+ # Correct the first offset if it isn't an exact match.
232
+ first_offset[:utc_offset] = first_transition.previous_offset.utc_offset
233
+ else
234
+ # Not transitioning from the designated first offset.
235
+ if offset_matches_rule?(first_offset, first_transition.offset)
236
+ # Correct the first offset if it isn't an exact match.
237
+ first_offset[:utc_offset] = first_transition.offset.utc_offset
238
+
239
+ # Skip an unnecessary transition to the first offset.
240
+ transitions.shift
241
+ end
242
+
243
+ # If the first offset doesn't match either the offset or previous
244
+ # offset, then it will be retained.
245
+ end
246
+
247
+ offset_indexes = get_rule_offset_indexes(offsets, rules)
248
+ convert_transitions_to_hashes(offset_indexes, transitions)
249
+ end
250
+ end
251
+
252
+ # Validates the rules offset against the offset of the last defined
253
+ # transition. Replaces the transition with an equivalent using the rules
254
+ # offset if the rules give a different definition for the base offset.
255
+ def replace_last_transition_offset_if_valid_and_needed(file, transitions, offsets)
256
+ last_transition = transitions.last
257
+ last_offset = offsets[last_transition[:offset]]
258
+ rule_offset = yield last_offset
259
+
260
+ unless offset_matches_rule?(last_offset, rule_offset)
261
+ raise InvalidZoneinfoFile, "Offset from POSIX-style TZ string does not match final transition in file '#{file.path}'."
262
+ end
263
+
264
+ # The total_utc_offset and abbreviation must always be the same. The
265
+ # base utc_offset and std_offset might differ. In which case the rule
266
+ # should be used as it will be more precise.
267
+ last_offset[:utc_offset] = rule_offset.utc_offset
268
+ last_transition
269
+ end
270
+
271
+ # todo: port over validate_and_fix_last_defined_transition_offset
272
+ # when fixing the previous offset will need to define a new one
273
+
274
+ # Validates the offset indicated to be observed by the rules before the
275
+ # first generated transition against the offset of the last defined
276
+ # transition.
277
+ #
278
+ # Fix the last defined transition if it differ on just base/std offsets
279
+ # (which are derived). Raise an error if the observed UTC offset or
280
+ # abbreviations differ.
281
+ def validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, first_rule_offset)
282
+ offset_of_last_defined = offsets[last_defined[:offset]]
283
+
284
+ if offset_equals_rule?(offset_of_last_defined, first_rule_offset)
285
+ last_defined
286
+ else
287
+ if offset_matches_rule?(offset_of_last_defined, first_rule_offset)
288
+ # The same overall offset, but differing in the base or std
289
+ # offset (which are derived). Correct by using the rule.
290
+
291
+ offset_index = get_rule_offset_index(offsets, first_rule_offset)
292
+ {:at => last_defined[:at], :offset => offset_index}
293
+ else
294
+ raise InvalidZoneinfoFile, "The first offset indicated by the POSIX-style TZ string did not match the final defined offset in file '#{file.path}'."
295
+ end
296
+ end
297
+ end
298
+
299
+ # Apply the rules from the TZ string when there were defined transitions.
300
+ # Checks for a matching offset with the last transition. Redefines the
301
+ # last transition if required and if the rules don't specific a constant
302
+ # offset, generates transitions until 100 years into the future (at the
303
+ # time of loading zoneinfo_timezone_info.rb) or 2037 if limited to 32-bit
304
+ # Times.
305
+ def apply_rules_with_transitions(file, transitions, offsets, first_offset_index, rules)
306
+ last_defined = transitions[-1]
307
+
308
+ if rules.kind_of?(TimezoneOffset)
309
+ transitions[-1] = validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, rules)
310
+ else
311
+ previous_offset_index = transitions.length > 1 ? transitions[-2][:offset] : first_offset_index
312
+ previous_offset = offsets[previous_offset_index]
313
+ last_year = (Time.at(last_defined[:at]).utc + previous_offset[:utc_total_offset]).year
314
+
315
+ if last_year <= GENERATE_UP_TO
316
+ last_defined_offset = offsets[last_defined[:offset]]
317
+
318
+ generated = rules.transitions(last_year).find_all do |t|
319
+ t.at > last_defined[:at] && !offset_matches_rule?(last_defined_offset, t.offset)
320
+ end
321
+
322
+ generated += (last_year + 1).upto(GENERATE_UP_TO).map {|y| rules.transitions(y) }.flatten
323
+
324
+ unless generated.empty?
325
+ transitions[-1] = validate_and_fix_last_defined_transition_offset(file, offsets, last_defined, generated[0].previous_offset)
326
+ rule_offset_indexes = get_rule_offset_indexes(offsets, rules)
327
+ transitions.concat(convert_transitions_to_hashes(rule_offset_indexes, generated))
328
+ end
329
+ end
330
+ end
331
+ end
332
+
142
333
  # Defines an offset for the timezone based on the given index and offset
143
334
  # Hash.
144
335
  def define_offset(index, offset)
@@ -164,21 +355,24 @@ module TZInfo
164
355
  end
165
356
 
166
357
  # Parses a zoneinfo file and intializes the DataTimezoneInfo structures.
167
- def parse(file)
168
- magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
358
+ def parse(file, posix_tz_parser)
359
+ magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
169
360
  check_read(file, 44).unpack('a4 a x15 NNNNNN')
170
361
 
171
362
  if magic != 'TZif'
172
363
  raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
173
364
  end
174
365
 
175
- if (version == '2' || version == '3') && RubyCoreSupport.time_supports_64bit
176
- # Skip the first 32-bit section and read the header of the second 64-bit section
177
- file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisgmtcnt + ttisstdcnt, IO::SEEK_CUR)
366
+ if version == '2' || version == '3'
367
+ # Skip the first 32-bit section and read the header of the second
368
+ # 64-bit section. The 64-bit section is always used even if the
369
+ # runtime platform doesn't support 64-bit timestamps. In "slim" format
370
+ # zoneinfo files the 32-bit section will be empty.
371
+ file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
178
372
 
179
373
  prev_version = version
180
374
 
181
- magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
375
+ magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
182
376
  check_read(file, 44).unpack('a4 a x15 NNNNNN')
183
377
 
184
378
  unless magic == 'TZif' && (version == prev_version)
@@ -226,7 +420,6 @@ module TZInfo
226
420
 
227
421
  unless isdst
228
422
  offset[:utc_offset] = gmtoff
229
- offset[:std_offset] = 0
230
423
  end
231
424
 
232
425
  offsets << offset
@@ -234,6 +427,23 @@ module TZInfo
234
427
 
235
428
  abbrev = check_read(file, charcnt)
236
429
 
430
+ if using_64bit
431
+ # Skip to the POSIX-style TZ string.
432
+ file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
433
+ tz_string_start = check_read(file, 1)
434
+ raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
435
+ tz_string = RubyCoreSupport.force_encoding(file.readline("\n"), 'UTF-8')
436
+ raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
437
+
438
+ begin
439
+ rules = posix_tz_parser.parse(tz_string)
440
+ rescue InvalidPosixTimeZone => e
441
+ raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
442
+ end
443
+ else
444
+ rules = nil
445
+ end
446
+
237
447
  offsets.each do |o|
238
448
  abbrev_start = o[:abbr_index]
239
449
  raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'" unless abbrev_start < abbrev.length
@@ -252,44 +462,58 @@ module TZInfo
252
462
 
253
463
  # Derive the offsets from standard time (std_offset).
254
464
  first_offset_index = derive_offsets(transitions, offsets)
255
-
256
- define_offset(first_offset_index, offsets[first_offset_index])
257
-
258
- offsets.each_with_index do |o, i|
259
- define_offset(i, o) unless i == first_offset_index
260
- end
261
465
 
262
- if !using_64bit && !RubyCoreSupport.time_supports_negative
263
- # Filter out transitions that are not supported by Time on this
264
- # platform.
265
-
266
- # Move the last transition before the epoch up to the epoch. This
267
- # allows for accurate conversions for all supported timestamps on the
268
- # platform.
466
+ # Filter out transitions that are not supported by Time on this
467
+ # platform.
468
+ unless transitions.empty?
469
+ if !RubyCoreSupport.time_supports_negative
470
+ transitions = remove_unsupported_negative_transitions(transitions, 0)
471
+ elsif !RubyCoreSupport.time_supports_64bit
472
+ transitions = remove_unsupported_negative_transitions(transitions, -2**31)
473
+ else
474
+ # Ignore transitions that occur outside of a defined window. The
475
+ # transition index cannot handle a large range of transition times.
476
+ #
477
+ # This is primarily intended to ignore the far in the past
478
+ # transition added in zic 2014c (at timestamp -2**63 in zic 2014c
479
+ # and at the approximate time of the big bang from zic 2014d).
480
+ #
481
+ # Assumes MIN_TIMESTAMP is less than -2**31.
482
+ transitions = remove_unsupported_negative_transitions(transitions, MIN_TIMESTAMP)
483
+ end
269
484
 
270
- before_epoch, after_epoch = transitions.partition {|t| t[:at] < 0}
485
+ if !RubyCoreSupport.time_supports_64bit
486
+ i = transitions.find_index {|t| t[:at] >= 2**31 }
487
+ had_later_transition = !!i
488
+ transitions = transitions.first(i) if i
489
+ else
490
+ had_later_transition = false
491
+ end
492
+ end
271
493
 
272
- if before_epoch.length > 0 && after_epoch.length > 0 && after_epoch.first[:at] != 0
273
- last_before = before_epoch.last
274
- last_before[:at] = 0
275
- transitions = [last_before] + after_epoch
494
+ if rules && !had_later_transition
495
+ if transitions.empty?
496
+ transitions = apply_rules_without_transitions(file, offsets, first_offset_index, rules)
276
497
  else
277
- transitions = after_epoch
498
+ apply_rules_with_transitions(file, transitions, offsets, first_offset_index, rules)
278
499
  end
279
500
  end
501
+
502
+ define_offset(first_offset_index, offsets[first_offset_index])
503
+
504
+ used_offset_indexes = transitions.map {|t| t[:offset] }.to_set
505
+
506
+ offsets.each_with_index do |o, i|
507
+ define_offset(i, o) if i != first_offset_index && used_offset_indexes.include?(i)
508
+ end
280
509
 
281
510
  # Ignore transitions that occur outside of a defined window. The
282
511
  # transition index cannot handle a large range of transition times.
283
- #
284
- # This is primarily intended to ignore the far in the past transition
285
- # added in zic 2014c (at timestamp -2**63 in zic 2014c and at the
286
- # approximate time of the big bang from zic 2014d).
287
512
  transitions.each do |t|
288
513
  at = t[:at]
289
- if at >= MIN_TIMESTAMP && at < MAX_TIMESTAMP
290
- time = Time.at(at).utc
291
- transition time.year, time.mon, t[:offset], at
292
- end
514
+ break if at >= MAX_TIMESTAMP
515
+ time = Time.at(at).utc
516
+ transition time.year, time.mon, t[:offset], at
293
517
  end
294
518
  end
295
519
  end