tzinfo 1.0.1 → 1.1.0

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 (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +6 -0
  5. data/{CHANGES → CHANGES.md} +121 -50
  6. data/{README → README.md} +48 -34
  7. data/Rakefile +45 -22
  8. data/lib/tzinfo.rb +7 -2
  9. data/lib/tzinfo/country.rb +23 -1
  10. data/lib/tzinfo/country_index_definition.rb +6 -1
  11. data/lib/tzinfo/country_timezone.rb +9 -1
  12. data/lib/tzinfo/data_source.rb +26 -5
  13. data/lib/tzinfo/data_timezone.rb +30 -2
  14. data/lib/tzinfo/data_timezone_info.rb +26 -0
  15. data/lib/tzinfo/info_timezone.rb +3 -1
  16. data/lib/tzinfo/linked_timezone.rb +30 -1
  17. data/lib/tzinfo/offset_rationals.rb +5 -3
  18. data/lib/tzinfo/ruby_core_support.rb +2 -0
  19. data/lib/tzinfo/ruby_country_info.rb +14 -0
  20. data/lib/tzinfo/ruby_data_source.rb +5 -0
  21. data/lib/tzinfo/time_or_datetime.rb +16 -1
  22. data/lib/tzinfo/timezone.rb +107 -5
  23. data/lib/tzinfo/timezone_definition.rb +5 -1
  24. data/lib/tzinfo/timezone_index_definition.rb +7 -1
  25. data/lib/tzinfo/{timezone_offset_info.rb → timezone_offset.rb} +12 -10
  26. data/lib/tzinfo/timezone_period.rb +19 -15
  27. data/lib/tzinfo/timezone_proxy.rb +5 -1
  28. data/lib/tzinfo/timezone_transition.rb +136 -0
  29. data/lib/tzinfo/{timezone_transition_info.rb → timezone_transition_definition.rb} +21 -57
  30. data/lib/tzinfo/transition_data_timezone_info.rb +81 -8
  31. data/lib/tzinfo/zoneinfo_country_info.rb +7 -0
  32. data/lib/tzinfo/zoneinfo_data_source.rb +104 -57
  33. data/lib/tzinfo/zoneinfo_timezone_info.rb +18 -10
  34. data/test/tc_country.rb +39 -1
  35. data/test/tc_data_timezone.rb +28 -5
  36. data/test/tc_linked_timezone.rb +24 -3
  37. data/test/tc_ruby_data_source.rb +22 -2
  38. data/test/tc_time_or_datetime.rb +3 -3
  39. data/test/tc_timezone.rb +364 -117
  40. data/test/tc_timezone_london.rb +25 -0
  41. data/test/tc_timezone_melbourne.rb +24 -0
  42. data/test/tc_timezone_new_york.rb +24 -0
  43. data/test/{tc_timezone_offset_info.rb → tc_timezone_offset.rb} +27 -27
  44. data/test/tc_timezone_period.rb +113 -90
  45. data/test/tc_timezone_transition.rb +374 -0
  46. data/test/tc_timezone_transition_definition.rb +306 -0
  47. data/test/tc_transition_data_timezone_info.rb +143 -43
  48. data/test/tc_zoneinfo_data_source.rb +69 -5
  49. data/test/tc_zoneinfo_timezone_info.rb +63 -20
  50. data/test/test_utils.rb +27 -7
  51. data/tzinfo.gemspec +21 -0
  52. metadata +61 -38
  53. metadata.gz.sig +3 -0
  54. data/test/tc_timezone_transition_info.rb +0 -471
  55. data/test/zoneinfo/UTC +0 -0
  56. data/test/zoneinfo/localtime +0 -0
@@ -22,6 +22,8 @@
22
22
 
23
23
  module TZInfo
24
24
  # Represents information about a country returned by ZoneinfoDataSource.
25
+ #
26
+ # @private
25
27
  class ZoneinfoCountryInfo < CountryInfo #:nodoc:
26
28
  # Constructs a new CountryInfo with an ISO 3166 country code, name and
27
29
  # an array of CountryTimezones.
@@ -34,6 +36,11 @@ module TZInfo
34
36
  # Returns a frozen array of all the zone identifiers for the country ordered
35
37
  # geographically, most populous first.
36
38
  def zone_identifiers
39
+ # Thread-safey: It is possible that the value of @zone_identifiers may be
40
+ # calculated multiple times in concurrently executing threads. It is not
41
+ # worth the overhead of locking to ensure that @zone_identifiers is only
42
+ # calculated once.
43
+
37
44
  unless @zone_identifiers
38
45
  @zone_identifiers = zones.collect {|zone| zone.identifier}.freeze
39
46
  end
@@ -48,7 +48,52 @@ module TZInfo
48
48
  # To load zoneinfo files from a particular directory, pass the directory to
49
49
  # TZInfo::DataSource.set:
50
50
  #
51
- # TZInfo::DataSource.set(:zoneinfo, directory)
51
+ # TZInfo::DataSource.set(:zoneinfo, directory)
52
+ #
53
+ # Note that the platform used at runtime may limit the range of available
54
+ # transition data that can be loaded from zoneinfo files. There are two
55
+ # factors to consider:
56
+ #
57
+ # First of all, the zoneinfo support in TZInfo makes use of Ruby's Time class.
58
+ # On 32-bit builds of Ruby 1.8, the Time class only supports 32-bit
59
+ # timestamps. This means that only Times between 1901-12-13 20:45:52 and
60
+ # 2038-01-19 03:14:07 can be represented. Furthermore, certain platforms only
61
+ # allow for positive 32-bit timestamps (notably Windows), making the earliest
62
+ # representable time 1970-01-01 00:00:00.
63
+ #
64
+ # 64-bit builds of Ruby 1.8 and all builds of Ruby 1.9 support 64-bit
65
+ # timestamps. This means that there is no practical restriction on the range
66
+ # of the Time class on these platforms.
67
+ #
68
+ # TZInfo will only load transitions that fall within the supported range of
69
+ # the Time class. Any queries performed on times outside of this range may
70
+ # give inaccurate results.
71
+ #
72
+ # The second factor concerns the zoneinfo files. Versions of the 'zic' tool
73
+ # (used to build zoneinfo files) that were released prior to February 2006
74
+ # created zoneinfo files that used 32-bit integers for transition timestamps.
75
+ # Later versions of zic produce zoneinfo files that use 64-bit integers. If
76
+ # you have 32-bit zoneinfo files on your system, then any queries falling
77
+ # outside of the range 1901-12-13 20:45:52 to 2038-01-19 03:14:07 may be
78
+ # inaccurate.
79
+ #
80
+ # Most modern platforms include 64-bit zoneinfo files. However, Mac OS X (up
81
+ # to at least 10.8.4) still uses 32-bit zoneinfo files.
82
+ #
83
+ # To check whether your zoneinfo files contain 32-bit or 64-bit transition
84
+ # data, you can run the following code (substituting the identifier of the
85
+ # zone you want to test for zone_identifier):
86
+ #
87
+ # TZInfo::DataSource.set(:zoneinfo)
88
+ # dir = TZInfo::DataSource.get.zoneinfo_dir
89
+ # File.open(File.join(dir, zone_identifier), 'r') {|f| f.read(5) }
90
+ #
91
+ # If the last line returns "TZif\\x00", then you have a 32-bit zoneinfo file.
92
+ # If it returns "TZif2" or "TZif3" then you have a 64-bit zoneinfo file.
93
+ #
94
+ # If you require support for 64-bit transitions, but are restricted to 32-bit
95
+ # zoneinfo support, then you may want to consider using TZInfo::RubyDataSource
96
+ # instead.
52
97
  class ZoneinfoDataSource < DataSource
53
98
  # The default value of ZoneInfoDataSource.search_path.
54
99
  DEFAULT_SEARCH_PATH = ['/usr/share/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo'].freeze
@@ -118,19 +163,23 @@ module TZInfo
118
163
 
119
164
  @zoneinfo_dir = File.expand_path(@zoneinfo_dir).freeze
120
165
  @zoneinfo_prefix = (@zoneinfo_dir + File::SEPARATOR).freeze
166
+ @timezone_index = load_timezone_index.freeze
167
+ @country_index = load_country_index.freeze
121
168
  end
122
169
 
123
170
  # Returns a TimezoneInfo instance for a given identifier.
124
171
  # Raises InvalidTimezoneIdentifier if the timezone is not found or the
125
172
  # identifier is invalid.
126
173
  def load_timezone_info(identifier)
127
- load_timezone_index
128
-
129
174
  begin
130
175
  if @timezone_index.include?(identifier)
131
- identifier.untaint
132
176
  path = File.join(@zoneinfo_dir, identifier)
133
177
 
178
+ # Untaint path rather than identifier. We don't want to modify
179
+ # identifier. identifier may also be frozen and therefore cannot be
180
+ # untainted.
181
+ path.untaint
182
+
134
183
  begin
135
184
  ZoneinfoTimezoneInfo.new(identifier, path)
136
185
  rescue InvalidZoneinfoFile => e
@@ -148,7 +197,6 @@ module TZInfo
148
197
 
149
198
  # Returns an array of all the available timezone identifiers.
150
199
  def timezone_identifiers
151
- load_timezone_index
152
200
  @timezone_index
153
201
  end
154
202
 
@@ -158,7 +206,6 @@ module TZInfo
158
206
  # For ZoneinfoDataSource, this will always be identical to
159
207
  # timezone_identifers.
160
208
  def data_timezone_identifiers
161
- load_timezone_index
162
209
  @timezone_index
163
210
  end
164
211
 
@@ -174,7 +221,6 @@ module TZInfo
174
221
  # country code. Raises InvalidCountryCode if the country could not be found
175
222
  # or the code is invalid.
176
223
  def load_country_info(code)
177
- load_country_index
178
224
  info = @country_index[code]
179
225
  raise InvalidCountryCode.new, 'Invalid country code' unless info
180
226
  info
@@ -183,7 +229,6 @@ module TZInfo
183
229
  # Returns an array of all the available ISO 3166-1 alpha-2
184
230
  # country codes.
185
231
  def country_codes
186
- load_country_index
187
232
  @country_index.keys.freeze
188
233
  end
189
234
 
@@ -205,18 +250,22 @@ module TZInfo
205
250
  File.directory?(path) && File.file?(File.join(path, 'zone.tab')) && File.file?(File.join(path, 'iso3166.tab'))
206
251
  end
207
252
 
208
- # Unless already called, scans @zoneinfo_dir looking for the available
209
- # timezone identifiers.
253
+ # Scans @zoneinfo_dir and returns an Array of available timezone
254
+ # identifiers.
210
255
  def load_timezone_index
211
- unless @timezone_index
212
- index = []
213
-
214
- enum_timezones(nil, ['localtime', 'posix', 'posixrules', 'right', 'Factory']) do |identifier|
215
- index << identifier
216
- end
217
-
218
- @timezone_index = index.sort.freeze
256
+ index = []
257
+
258
+ # Ignoring particular files:
259
+ # +VERSION is included in Mac OS X.
260
+ # localtime current local timezone (may be a link).
261
+ # posix, posixrules and right are directories containing other versions of the zoneinfo files.
262
+ # Factory is the compiled in default timezone.
263
+
264
+ enum_timezones(nil, ['+VERSION', 'localtime', 'posix', 'posixrules', 'right', 'Factory']) do |identifier|
265
+ index << identifier
219
266
  end
267
+
268
+ index.sort
220
269
  end
221
270
 
222
271
  # Recursively scans a directory of timezones, calling the passed in block
@@ -237,53 +286,51 @@ module TZInfo
237
286
  end
238
287
  end
239
288
 
240
- # Unless called before, uses the iso3166.tab and zone.tab files to build
241
- # an index of the available countries and their timezones.
289
+ # Uses the iso3166.tab and zone.tab files to build an index of the
290
+ # available countries and their timezones.
242
291
  def load_country_index
243
- unless @country_index
244
- zones = {}
245
-
246
- File.open(File.join(@zoneinfo_dir, 'zone.tab')) do |file|
247
- file.each_line do |line|
248
- line.chomp!
292
+ zones = {}
293
+
294
+ File.open(File.join(@zoneinfo_dir, 'zone.tab')) do |file|
295
+ file.each_line do |line|
296
+ line.chomp!
297
+
298
+ if line =~ /\A([A-Z]{2})\t(?:([+\-])(\d{2})(\d{2})([+\-])(\d{3})(\d{2})|([+\-])(\d{2})(\d{2})(\d{2})([+\-])(\d{3})(\d{2})(\d{2}))\t([^\t]+)(?:\t([^\t]+))?\z/
299
+ code = $1
249
300
 
250
- if line =~ /\A([A-Z]{2})\t(?:([+\-])(\d{2})(\d{2})([+\-])(\d{3})(\d{2})|([+\-])(\d{2})(\d{2})(\d{2})([+\-])(\d{3})(\d{2})(\d{2}))\t([^\t]+)(?:\t([^\t]+))?\z/
251
- code = $1
252
-
253
- if $2
254
- latitude = dms_to_rational($2, $3, $4)
255
- longitude = dms_to_rational($5, $6, $7)
256
- else
257
- latitude = dms_to_rational($8, $9, $10, $11)
258
- longitude = dms_to_rational($12, $13, $14, $15)
259
- end
260
-
261
- zone_identifier = $16
262
- description = $17
263
-
264
- (zones[code] ||= []) <<
265
- CountryTimezone.new(zone_identifier, latitude.numerator, latitude.denominator,
266
- longitude.numerator, longitude.denominator, description)
301
+ if $2
302
+ latitude = dms_to_rational($2, $3, $4)
303
+ longitude = dms_to_rational($5, $6, $7)
304
+ else
305
+ latitude = dms_to_rational($8, $9, $10, $11)
306
+ longitude = dms_to_rational($12, $13, $14, $15)
267
307
  end
308
+
309
+ zone_identifier = $16
310
+ description = $17
311
+
312
+ (zones[code] ||= []) <<
313
+ CountryTimezone.new(zone_identifier, latitude.numerator, latitude.denominator,
314
+ longitude.numerator, longitude.denominator, description)
268
315
  end
269
316
  end
270
-
271
- countries = {}
272
-
273
- File.open(File.join(@zoneinfo_dir, 'iso3166.tab')) do |file|
274
- file.each_line do |line|
275
- line.chomp!
276
-
277
- if line =~ /\A([A-Z]{2})\t(.+)\z/
278
- code = $1
279
- name = $2
280
- countries[code] = ZoneinfoCountryInfo.new(code, name, zones[code] || [])
281
- end
317
+ end
318
+
319
+ countries = {}
320
+
321
+ File.open(File.join(@zoneinfo_dir, 'iso3166.tab')) do |file|
322
+ file.each_line do |line|
323
+ line.chomp!
324
+
325
+ if line =~ /\A([A-Z]{2})\t(.+)\z/
326
+ code = $1
327
+ name = $2
328
+ countries[code] = ZoneinfoCountryInfo.new(code, name, zones[code] || [])
282
329
  end
283
330
  end
284
-
285
- @country_index = countries
286
331
  end
332
+
333
+ countries
287
334
  end
288
335
 
289
336
  # Converts degrees, miunutes and seconds to a Rational
@@ -27,6 +27,8 @@ module TZInfo
27
27
  end
28
28
 
29
29
  # Represents a timezone defined by a compiled zoneinfo TZif (\0 or 2) file.
30
+ #
31
+ # @private
30
32
  class ZoneinfoTimezoneInfo < TransitionDataTimezoneInfo #:nodoc:
31
33
 
32
34
  # Constructs the new ZoneinfoTimezoneInfo with an identifier and path
@@ -103,23 +105,29 @@ module TZInfo
103
105
 
104
106
  # Parses a zoneinfo file and intializes the DataTimezoneInfo structures.
105
107
  def parse(file)
106
- magic, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
107
- check_read(file, 44).unpack('a5 x15 NNNNNN')
108
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
109
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
108
110
 
109
- if magic == 'TZif2' && RubyCoreSupport.time_supports_64bit
111
+ if magic != 'TZif'
112
+ raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
113
+ end
114
+
115
+ if (version == '2' || version == '3') && RubyCoreSupport.time_supports_64bit
110
116
  # Skip the first 32-bit section and read the header of the second 64-bit section
111
117
  check_read(file, timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisgmtcnt + ttisstdcnt)
112
118
 
113
- magic, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
114
- check_read(file, 44).unpack('a5 x15 NNNNNN')
119
+ prev_version = version
120
+
121
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
122
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
115
123
 
116
- unless magic == 'TZif2'
117
- raise InvalidZoneinfoFile, "Invalid magic in second section of file '#{file.path}'"
124
+ unless magic == 'TZif' && (version == prev_version)
125
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
118
126
  end
119
127
 
120
- using_64bit = true
121
- elsif magic != 'TZif2' && magic != "TZif\0"
122
- raise InvalidZoneinfoFile, "Invalid magic in file '#{file.path}'"
128
+ using_64bit = true
129
+ elsif version != '3' && version != '2' && version != "\0"
130
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
123
131
  else
124
132
  using_64bit = false
125
133
  end
data/test/tc_country.rb CHANGED
@@ -27,7 +27,7 @@ include TZInfo
27
27
  class TCCountry < Test::Unit::TestCase
28
28
  def setup
29
29
  @orig_data_source = DataSource.get
30
- Country.send :class_variable_set, :@@countries, {}
30
+ Country.send :init_countries
31
31
  end
32
32
 
33
33
  def teardown
@@ -64,6 +64,44 @@ class TCCountry < Test::Unit::TestCase
64
64
  Country.get('gb')
65
65
  }
66
66
  end
67
+
68
+ def test_get_tainted_loaded
69
+ Country.get('GB')
70
+
71
+ safe_test do
72
+ code = 'GB'.taint
73
+ assert(code.tainted?)
74
+ country = Country.get(code)
75
+ assert_equal('GB', country.code)
76
+ assert(code.tainted?)
77
+ end
78
+ end
79
+
80
+ def test_get_tainted_and_frozen_loaded
81
+ Country.get('GB')
82
+
83
+ safe_test do
84
+ country = Country.get('GB'.taint.freeze)
85
+ assert_equal('GB', country.code)
86
+ end
87
+ end
88
+
89
+ def test_get_tainted_not_previously_loaded
90
+ safe_test do
91
+ code = 'GB'.taint
92
+ assert(code.tainted?)
93
+ country = Country.get(code)
94
+ assert_equal('GB', country.code)
95
+ assert(code.tainted?)
96
+ end
97
+ end
98
+
99
+ def test_get_tainted_and_frozen_not_previously_loaded
100
+ safe_test do
101
+ country = Country.get('GB'.taint.freeze)
102
+ assert_equal('GB', country.code)
103
+ end
104
+ end
67
105
 
68
106
  def test_new_nil
69
107
  assert_raises(InvalidCountryCode) {
@@ -29,11 +29,14 @@ class TCDataTimezone < Test::Unit::TestCase
29
29
  class TestTimezoneInfo < TimezoneInfo
30
30
  attr_reader :utc
31
31
  attr_reader :local
32
+ attr_reader :utc_to
33
+ attr_reader :utc_from
32
34
 
33
- def initialize(identifier, utc_period, local_periods)
35
+ def initialize(identifier, utc_period, local_periods, transitions_up_to)
34
36
  super(identifier)
35
37
  @utc_period = utc_period
36
38
  @local_periods = local_periods || []
39
+ @transitions_up_to = transitions_up_to
37
40
  end
38
41
 
39
42
  def period_for_utc(utc)
@@ -45,10 +48,16 @@ class TCDataTimezone < Test::Unit::TestCase
45
48
  @local = local
46
49
  @local_periods
47
50
  end
51
+
52
+ def transitions_up_to(utc_to, utc_from = nil)
53
+ @utc_to = utc_to
54
+ @utc_from = utc_from
55
+ @transitions_up_to
56
+ end
48
57
  end
49
58
 
50
59
  def test_identifier
51
- tz = DataTimezone.new(TestTimezoneInfo.new('Test/Zone', nil, []))
60
+ tz = DataTimezone.new(TestTimezoneInfo.new('Test/Zone', nil, [], []))
52
61
  assert_equal('Test/Zone', tz.identifier)
53
62
  end
54
63
 
@@ -56,7 +65,7 @@ class TCDataTimezone < Test::Unit::TestCase
56
65
  # Don't need actual TimezonePeriods. DataTimezone isn't supposed to do
57
66
  # anything with them apart from return them.
58
67
  period = Object.new
59
- tti = TestTimezoneInfo.new('Test/Zone', period, [])
68
+ tti = TestTimezoneInfo.new('Test/Zone', period, [], [])
60
69
  tz = DataTimezone.new(tti)
61
70
 
62
71
  t = Time.utc(2006, 6, 27, 22, 50, 12)
@@ -68,7 +77,7 @@ class TCDataTimezone < Test::Unit::TestCase
68
77
  # Don't need actual TimezonePeriods. DataTimezone isn't supposed to do
69
78
  # anything with them apart from return them.
70
79
  periods = [Object.new, Object.new]
71
- tti = TestTimezoneInfo.new('Test/Zone', nil, periods)
80
+ tti = TestTimezoneInfo.new('Test/Zone', nil, periods, [])
72
81
  tz = DataTimezone.new(tti)
73
82
 
74
83
  t = Time.utc(2006, 6, 27, 22, 50, 12)
@@ -78,11 +87,25 @@ class TCDataTimezone < Test::Unit::TestCase
78
87
 
79
88
  def test_periods_for_local_not_found
80
89
  periods = []
81
- tti = TestTimezoneInfo.new('Test/Zone', nil, periods)
90
+ tti = TestTimezoneInfo.new('Test/Zone', nil, periods, [])
82
91
  tz = DataTimezone.new(tti)
83
92
 
84
93
  t = Time.utc(2006, 6, 27, 22, 50, 12)
85
94
  assert_same(periods, tz.periods_for_local(t))
86
95
  assert_same(t, tti.local)
96
+ end
97
+
98
+ def test_transitions_up_to
99
+ # Don't need actual TimezoneTransition instances. DataTimezone isn't
100
+ # supposed to do anything with them apart from return them.
101
+ transitions = [Object.new, Object.new]
102
+ tti = TestTimezoneInfo.new('Test/Zone', nil, nil, transitions)
103
+ tz = DataTimezone.new(tti)
104
+
105
+ utc_to = Time.utc(2013, 1, 1, 0, 0, 0)
106
+ utc_from = Time.utc(2012, 1, 1, 0, 0, 0)
107
+ assert_same(transitions, tz.transitions_up_to(utc_to, utc_from))
108
+ assert_same(utc_to, tti.utc_to)
109
+ assert_same(utc_from, tti.utc_from)
87
110
  end
88
111
  end
@@ -29,8 +29,11 @@ class TCLinkedTimezone < Test::Unit::TestCase
29
29
  class TestTimezone < Timezone
30
30
  attr_reader :utc_period
31
31
  attr_reader :local_periods
32
+ attr_reader :up_to_transitions
32
33
  attr_reader :utc
33
- attr_reader :local
34
+ attr_reader :local
35
+ attr_reader :utc_to
36
+ attr_reader :utc_from
34
37
 
35
38
  def self.new(identifier, no_local_periods = false)
36
39
  tz = super()
@@ -53,14 +56,22 @@ class TCLinkedTimezone < Test::Unit::TestCase
53
56
  @local_periods
54
57
  end
55
58
 
59
+ def transitions_up_to(utc_to, utc_from = nil)
60
+ @utc_to = utc_to
61
+ @utc_from = utc_from
62
+ @up_to_transitions
63
+ end
64
+
56
65
  private
57
66
  def setup(identifier, no_local_periods)
58
67
  @identifier = identifier
59
68
  @no_local_periods = no_local_periods
60
69
 
61
- # Doesn't have to be a real TimezonePeriod (nothing attempts to use it).
70
+ # Don't have to be real TimezonePeriod or TimezoneTransition objects
71
+ # (nothing will use them).
62
72
  @utc_period = Object.new
63
73
  @local_periods = [Object.new, Object.new]
74
+ @up_to_transitions = [Object.new, Object.new]
64
75
  end
65
76
  end
66
77
 
@@ -117,5 +128,15 @@ class TCLinkedTimezone < Test::Unit::TestCase
117
128
  t = Time.utc(2006, 6, 27, 23, 12, 28)
118
129
  assert_raises(PeriodNotFound) { tz.periods_for_local(t) }
119
130
  assert_same(t, linked_tz.local)
120
- end
131
+ end
132
+
133
+ def test_transitions_up_to
134
+ tz = LinkedTimezone.new(LinkedTimezoneInfo.new('Test/Zone', 'Test/Linked'))
135
+ linked_tz = Timezone.get('Test/Linked')
136
+ utc_to = Time.utc(2013, 1, 1, 0, 0, 0)
137
+ utc_from = Time.utc(2012, 1, 1, 0, 0, 0)
138
+ assert_same(linked_tz.up_to_transitions, tz.transitions_up_to(utc_to, utc_from))
139
+ assert_same(utc_to, linked_tz.utc_to)
140
+ assert_same(utc_from, linked_tz.utc_from)
141
+ end
121
142
  end