tzinfo 1.2.7 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +489 -382
  6. data/LICENSE +12 -12
  7. data/README.md +368 -114
  8. data/lib/tzinfo.rb +59 -29
  9. data/lib/tzinfo/country.rb +141 -129
  10. data/lib/tzinfo/country_timezone.rb +70 -112
  11. data/lib/tzinfo/data_source.rb +389 -144
  12. data/lib/tzinfo/data_sources.rb +8 -0
  13. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  14. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  15. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  16. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  17. data/lib/tzinfo/data_sources/ruby_data_source.rb +145 -0
  18. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  19. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  20. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +577 -0
  21. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +288 -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.rb +10 -0
  25. data/lib/tzinfo/format1/country_definer.rb +17 -0
  26. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  27. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  28. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  29. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  30. data/lib/tzinfo/format2.rb +10 -0
  31. data/lib/tzinfo/format2/country_definer.rb +68 -0
  32. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  33. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  34. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  35. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  36. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  37. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -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 +128 -0
  43. data/lib/tzinfo/timestamp.rb +548 -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/transitions_timezone_period.rb +63 -0
  51. data/lib/tzinfo/untaint_ext.rb +18 -0
  52. data/lib/tzinfo/version.rb +7 -0
  53. data/lib/tzinfo/with_offset.rb +61 -0
  54. metadata +42 -98
  55. metadata.gz.sig +0 -0
  56. data/Rakefile +0 -107
  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/ruby_core_support.rb +0 -169
  63. data/lib/tzinfo/ruby_country_info.rb +0 -74
  64. data/lib/tzinfo/ruby_data_source.rb +0 -140
  65. data/lib/tzinfo/time_or_datetime.rb +0 -340
  66. data/lib/tzinfo/timezone_definition.rb +0 -36
  67. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  68. data/lib/tzinfo/timezone_info.rb +0 -30
  69. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  70. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  71. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  72. data/lib/tzinfo/zoneinfo_data_source.rb +0 -496
  73. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -300
  74. data/test/tc_country.rb +0 -238
  75. data/test/tc_country_index_definition.rb +0 -69
  76. data/test/tc_country_info.rb +0 -16
  77. data/test/tc_country_timezone.rb +0 -173
  78. data/test/tc_data_source.rb +0 -218
  79. data/test/tc_data_timezone.rb +0 -99
  80. data/test/tc_data_timezone_info.rb +0 -18
  81. data/test/tc_info_timezone.rb +0 -34
  82. data/test/tc_linked_timezone.rb +0 -155
  83. data/test/tc_linked_timezone_info.rb +0 -23
  84. data/test/tc_offset_rationals.rb +0 -23
  85. data/test/tc_ruby_core_support.rb +0 -168
  86. data/test/tc_ruby_country_info.rb +0 -110
  87. data/test/tc_ruby_data_source.rb +0 -167
  88. data/test/tc_time_or_datetime.rb +0 -660
  89. data/test/tc_timezone.rb +0 -1361
  90. data/test/tc_timezone_definition.rb +0 -113
  91. data/test/tc_timezone_index_definition.rb +0 -73
  92. data/test/tc_timezone_info.rb +0 -11
  93. data/test/tc_timezone_london.rb +0 -143
  94. data/test/tc_timezone_melbourne.rb +0 -142
  95. data/test/tc_timezone_new_york.rb +0 -142
  96. data/test/tc_timezone_offset.rb +0 -126
  97. data/test/tc_timezone_period.rb +0 -555
  98. data/test/tc_timezone_proxy.rb +0 -136
  99. data/test/tc_timezone_transition.rb +0 -366
  100. data/test/tc_timezone_transition_definition.rb +0 -295
  101. data/test/tc_timezone_utc.rb +0 -27
  102. data/test/tc_transition_data_timezone_info.rb +0 -433
  103. data/test/tc_zoneinfo_country_info.rb +0 -78
  104. data/test/tc_zoneinfo_data_source.rb +0 -1204
  105. data/test/tc_zoneinfo_timezone_info.rb +0 -1236
  106. data/test/test_utils.rb +0 -192
  107. data/test/ts_all.rb +0 -7
  108. data/test/ts_all_ruby.rb +0 -5
  109. data/test/ts_all_zoneinfo.rb +0 -9
  110. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  111. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  112. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -315
  113. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -218
  114. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  115. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  116. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  117. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  118. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -261
  119. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -186
  120. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -321
  121. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -265
  122. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -220
  123. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  124. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -927
  125. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -596
  126. data/test/tzinfo-data/tzinfo/data/version.rb +0 -14
  127. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  128. data/test/zoneinfo/America/New_York +0 -0
  129. data/test/zoneinfo/Australia/Melbourne +0 -0
  130. data/test/zoneinfo/EST +0 -0
  131. data/test/zoneinfo/Etc/UTC +0 -0
  132. data/test/zoneinfo/Europe/Amsterdam +0 -0
  133. data/test/zoneinfo/Europe/Andorra +0 -0
  134. data/test/zoneinfo/Europe/London +0 -0
  135. data/test/zoneinfo/Europe/Paris +0 -0
  136. data/test/zoneinfo/Europe/Prague +0 -0
  137. data/test/zoneinfo/Factory +0 -0
  138. data/test/zoneinfo/iso3166.tab +0 -275
  139. data/test/zoneinfo/leapseconds +0 -61
  140. data/test/zoneinfo/posix/Europe/London +0 -0
  141. data/test/zoneinfo/posixrules +0 -0
  142. data/test/zoneinfo/right/Europe/London +0 -0
  143. data/test/zoneinfo/zone.tab +0 -439
  144. data/test/zoneinfo/zone1970.tab +0 -369
  145. data/tzinfo.gemspec +0 -21
@@ -0,0 +1,288 @@
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
+ # Initializes a new {ZoneinfoReader}.
18
+ #
19
+ # @param string_deduper [StringDeduper] a {StringDeduper} instance to use
20
+ # when deduping abbreviations.
21
+ def initialize(string_deduper)
22
+ @string_deduper = string_deduper
23
+ end
24
+
25
+ # Reads a zoneinfo structure from the given path. Returns either a
26
+ # {TimezoneOffset} that is constantly observed or an `Array`
27
+ # {TimezoneTransition}s.
28
+ #
29
+ # @param file_path [String] the path of a zoneinfo file.
30
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
31
+ # {TimezoneTransition}s.
32
+ # @raise [SecurityError] if safe mode is enabled and `file_path` is
33
+ # tainted.
34
+ # @raise [InvalidZoneinfoFile] if `file_path`` does not refer to a valid
35
+ # zoneinfo file.
36
+ def read(file_path)
37
+ File.open(file_path, 'rb') { |file| parse(file) }
38
+ end
39
+
40
+ private
41
+
42
+ # Translates an unsigned 32-bit integer (as returned by unpack) to signed
43
+ # 32-bit.
44
+ #
45
+ # @param long [Integer] an unsigned 32-bit integer.
46
+ # @return [Integer] {long} translated to signed 32-bit.
47
+ def make_signed_int32(long)
48
+ long >= 0x80000000 ? long - 0x100000000 : long
49
+ end
50
+
51
+ # Translates a pair of unsigned 32-bit integers (as returned by unpack,
52
+ # most significant first) to a signed 64-bit integer.
53
+ #
54
+ # @param high [Integer] the most significant 32-bits.
55
+ # @param low [Integer] the least significant 32-bits.
56
+ # @return [Integer] {high} and {low} combined and translated to signed
57
+ # 64-bit.
58
+ def make_signed_int64(high, low)
59
+ unsigned = (high << 32) | low
60
+ unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
61
+ end
62
+
63
+ # Reads the given number of bytes from the given file and checks that the
64
+ # correct number of bytes could be read.
65
+ #
66
+ # @param file [IO] the file to read from.
67
+ # @param bytes [Integer] the number of bytes to read.
68
+ # @return [String] the bytes that were read.
69
+ # @raise [InvalidZoneinfoFile] if the number of bytes available didn't
70
+ # match the number requested.
71
+ def check_read(file, bytes)
72
+ result = file.read(bytes)
73
+
74
+ unless result && result.length == bytes
75
+ raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
76
+ end
77
+
78
+ result
79
+ end
80
+
81
+ # Zoneinfo files don't include the offset from standard time (std_offset)
82
+ # for DST periods. Derive the base offset (base_utc_offset) where DST is
83
+ # observed from either the previous or next non-DST period.
84
+ #
85
+ # @param transitions [Array<Hash>] an `Array` of transition hashes.
86
+ # @param offsets [Array<Hash>] an `Array` of offset hashes.
87
+ # @return [Integer] the index of the offset to be used prior to the first
88
+ # transition.
89
+ def derive_offsets(transitions, offsets)
90
+ # The first non-DST offset (if there is one) is the offset observed
91
+ # before the first transition. Fall back to the first DST offset if
92
+ # there are no non-DST offsets.
93
+ first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
94
+ first_offset_index = first_non_dst_offset_index || 0
95
+ return first_offset_index if transitions.empty?
96
+
97
+ # Determine the base_utc_offset of the next non-dst offset at each transition.
98
+ base_utc_offset_from_next = nil
99
+
100
+ transitions.reverse_each do |transition|
101
+ offset = offsets[transition[:offset]]
102
+ if offset[:is_dst]
103
+ transition[:base_utc_offset_from_next] = base_utc_offset_from_next if base_utc_offset_from_next
104
+ else
105
+ base_utc_offset_from_next = offset[:observed_utc_offset]
106
+ end
107
+ end
108
+
109
+ base_utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:observed_utc_offset] : nil
110
+ defined_offsets = {}
111
+
112
+ transitions.each do |transition|
113
+ offset_index = transition[:offset]
114
+ offset = offsets[offset_index]
115
+ observed_utc_offset = offset[:observed_utc_offset]
116
+
117
+ if offset[:is_dst]
118
+ base_utc_offset_from_next = transition[:base_utc_offset_from_next]
119
+
120
+ difference_to_previous = (observed_utc_offset - (base_utc_offset_from_previous || observed_utc_offset)).abs
121
+ difference_to_next = (observed_utc_offset - (base_utc_offset_from_next || observed_utc_offset)).abs
122
+
123
+ base_utc_offset = if difference_to_previous == 3600
124
+ base_utc_offset_from_previous
125
+ elsif difference_to_next == 3600
126
+ base_utc_offset_from_next
127
+ elsif difference_to_previous > 0 && difference_to_next > 0
128
+ difference_to_previous < difference_to_next ? base_utc_offset_from_previous : base_utc_offset_from_next
129
+ elsif difference_to_previous > 0
130
+ base_utc_offset_from_previous
131
+ elsif difference_to_next > 0
132
+ base_utc_offset_from_next
133
+ else
134
+ # No difference, assume a 1 hour offset from standard time.
135
+ observed_utc_offset - 3600
136
+ end
137
+
138
+ if !offset[:base_utc_offset]
139
+ offset[:base_utc_offset] = base_utc_offset
140
+ defined_offsets[offset] = offset_index
141
+ elsif offset[:base_utc_offset] != base_utc_offset
142
+ # An earlier transition has already derived a different
143
+ # base_utc_offset. Define a new offset or reuse an existing identically
144
+ # defined offset.
145
+ new_offset = offset.dup
146
+ new_offset[:base_utc_offset] = base_utc_offset
147
+
148
+ offset_index = defined_offsets[new_offset]
149
+
150
+ unless offset_index
151
+ offsets << new_offset
152
+ offset_index = offsets.length - 1
153
+ defined_offsets[new_offset] = offset_index
154
+ end
155
+
156
+ transition[:offset] = offset_index
157
+ end
158
+ else
159
+ base_utc_offset_from_previous = observed_utc_offset
160
+ end
161
+ end
162
+
163
+ first_offset_index
164
+ end
165
+
166
+ # Parses a zoneinfo file and returns either a {TimezoneOffset} that is
167
+ # constantly observed or an `Array` of {TimezoneTransition}s.
168
+ #
169
+ # @param file [IO] the file to be read.
170
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
171
+ # {TimezoneTransition}s.
172
+ # @raise [InvalidZoneinfoFile] if the file is not a valid zoneinfo file.
173
+ def parse(file)
174
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
175
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
176
+
177
+ if magic != 'TZif'
178
+ raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
179
+ end
180
+
181
+ if version == '2' || version == '3'
182
+ # Skip the first 32-bit section and read the header of the second 64-bit section
183
+ file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisgmtcnt + ttisstdcnt, IO::SEEK_CUR)
184
+
185
+ prev_version = version
186
+
187
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
188
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
189
+
190
+ unless magic == 'TZif' && (version == prev_version)
191
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
192
+ end
193
+
194
+ using_64bit = true
195
+ elsif version != '3' && version != '2' && version != "\0"
196
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
197
+ else
198
+ using_64bit = false
199
+ end
200
+
201
+ unless leapcnt == 0
202
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
203
+ end
204
+
205
+ transitions = if using_64bit
206
+ timecnt.times.map do |i|
207
+ high, low = check_read(file, 8).unpack('NN'.freeze)
208
+ transition_time = make_signed_int64(high, low)
209
+ {at: transition_time}
210
+ end
211
+ else
212
+ timecnt.times.map do |i|
213
+ transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
214
+ {at: transition_time}
215
+ end
216
+ end
217
+
218
+ check_read(file, timecnt).unpack('C*'.freeze).each_with_index do |localtime_type, i|
219
+ raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'." if localtime_type >= typecnt
220
+ transitions[i][:offset] = localtime_type
221
+ end
222
+
223
+ offsets = typecnt.times.map do |i|
224
+ gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
225
+ gmtoff = make_signed_int32(gmtoff)
226
+ isdst = isdst == 1
227
+ {observed_utc_offset: gmtoff, is_dst: isdst, abbr_index: abbrind}
228
+ end
229
+
230
+ abbrev = check_read(file, charcnt)
231
+
232
+ # Derive the offsets from standard time (std_offset).
233
+ first_offset_index = derive_offsets(transitions, offsets)
234
+
235
+ offsets = offsets.map do |o|
236
+ observed_utc_offset = o[:observed_utc_offset]
237
+ base_utc_offset = o[:base_utc_offset]
238
+
239
+ if base_utc_offset
240
+ # DST offset with base_utc_offset derived by derive_offsets.
241
+ std_offset = observed_utc_offset - base_utc_offset
242
+ elsif o[:is_dst]
243
+ # DST offset unreferenced by a transition (offset in use before the
244
+ # first transition). No derived base UTC offset, so assume 1 hour
245
+ # DST.
246
+ base_utc_offset = observed_utc_offset - 3600
247
+ std_offset = 3600
248
+ else
249
+ # Non-DST offset.
250
+ base_utc_offset = observed_utc_offset
251
+ std_offset = 0
252
+ end
253
+
254
+ abbrev_start = o[:abbr_index]
255
+ raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'." unless abbrev_start < abbrev.length
256
+
257
+ abbrev_end = abbrev.index("\0", abbrev_start)
258
+ raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'." unless abbrev_end
259
+
260
+ abbr = @string_deduper.dedupe(abbrev[abbrev_start...abbrev_end].force_encoding(Encoding::UTF_8).untaint)
261
+
262
+ TimezoneOffset.new(base_utc_offset, std_offset, abbr)
263
+ end
264
+
265
+ first_offset = offsets[first_offset_index]
266
+
267
+
268
+ if transitions.empty?
269
+ first_offset
270
+ else
271
+ previous_offset = first_offset
272
+ previous_at = nil
273
+
274
+ transitions.map do |t|
275
+ offset = offsets[t[:offset]]
276
+ at = t[:at]
277
+ 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
278
+ tt = TimezoneTransition.new(offset, previous_offset, at)
279
+ previous_offset = offset
280
+ previous_at = at
281
+ tt
282
+ end
283
+ end
284
+ end
285
+ end
286
+ private_constant :ZoneinfoReader
287
+ end
288
+ 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
@@ -0,0 +1,153 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'date'
5
+
6
+ module TZInfo
7
+ # A subclass of `DateTime` used to represent local times. {DateTimeWithOffset}
8
+ # holds a reference to the related {TimezoneOffset} and overrides various
9
+ # methods to return results appropriate for the {TimezoneOffset}. Certain
10
+ # operations will clear the associated {TimezoneOffset} (if the
11
+ # {TimezoneOffset} would not necessarily be valid for the result). Once the
12
+ # {TimezoneOffset} has been cleared, {DateTimeWithOffset} behaves identically
13
+ # to `DateTime`.
14
+ #
15
+ # Arithmetic performed on {DateTimeWithOffset} instances is _not_ time
16
+ # zone-aware. Regardless of whether transitions in the time zone are crossed,
17
+ # results of arithmetic operations will always maintain the same offset from
18
+ # UTC (`offset`). The associated {TimezoneOffset} will aways be cleared.
19
+ class DateTimeWithOffset < DateTime
20
+ include WithOffset
21
+
22
+ # @return [TimezoneOffset] the {TimezoneOffset} associated with this
23
+ # instance.
24
+ attr_reader :timezone_offset
25
+
26
+ # Sets the associated {TimezoneOffset}.
27
+ #
28
+ # @param timezone_offset [TimezoneOffset] a {TimezoneOffset} valid at the
29
+ # time and for the offset of this {DateTimeWithOffset}.
30
+ # @return [DateTimeWithOffset] `self`.
31
+ # @raise [ArgumentError] if `timezone_offset` is `nil`.
32
+ # @raise [ArgumentError] if `timezone_offset.observed_utc_offset` does not
33
+ # equal `self.offset * 86400`.
34
+ def set_timezone_offset(timezone_offset)
35
+ raise ArgumentError, 'timezone_offset must be specified' unless timezone_offset
36
+ raise ArgumentError, 'timezone_offset.observed_utc_offset does not match self.utc_offset' if offset * 86400 != timezone_offset.observed_utc_offset
37
+ @timezone_offset = timezone_offset
38
+ self
39
+ end
40
+
41
+ # An overridden version of `DateTime#to_time` that, if there is an
42
+ # associated {TimezoneOffset}, returns a {DateTimeWithOffset} with that
43
+ # offset.
44
+ #
45
+ # @return [Time] if there is an associated {TimezoneOffset}, a
46
+ # {TimeWithOffset} representation of this {DateTimeWithOffset}, otherwise
47
+ # a `Time` representation.
48
+ def to_time
49
+ if_timezone_offset(super) do |o,t|
50
+ # Ruby 2.4.0 changed the behaviour of to_time so that it preserves the
51
+ # offset instead of converting to the system local timezone.
52
+ #
53
+ # When self has an associated TimezonePeriod, this implementation will
54
+ # preserve the offset on all versions of Ruby.
55
+ TimeWithOffset.at(t.to_i, t.subsec * 1_000_000).set_timezone_offset(o)
56
+ end
57
+ end
58
+
59
+ # An overridden version of `DateTime#downto` that clears the associated
60
+ # {TimezoneOffset} of the returned or yielded instances.
61
+ def downto(min)
62
+ if block_given?
63
+ super {|dt| yield dt.clear_timezone_offset }
64
+ else
65
+ enum = super
66
+ enum.each {|dt| dt.clear_timezone_offset }
67
+ enum
68
+ end
69
+ end
70
+
71
+ # An overridden version of `DateTime#england` that preserves the associated
72
+ # {TimezoneOffset}.
73
+ #
74
+ # @return [DateTime]
75
+ def england
76
+ # super doesn't call #new_start on MRI, so each method has to be
77
+ # individually overridden.
78
+ if_timezone_offset(super) {|o,dt| dt.set_timezone_offset(o) }
79
+ end
80
+
81
+ # An overridden version of `DateTime#gregorian` that preserves the
82
+ # associated {TimezoneOffset}.
83
+ #
84
+ # @return [DateTime]
85
+ def gregorian
86
+ # super doesn't call #new_start on MRI, so each method has to be
87
+ # individually overridden.
88
+ if_timezone_offset(super) {|o,dt| dt.set_timezone_offset(o) }
89
+ end
90
+
91
+ # An overridden version of `DateTime#italy` that preserves the associated
92
+ # {TimezoneOffset}.
93
+ #
94
+ # @return [DateTime]
95
+ def italy
96
+ # super doesn't call #new_start on MRI, so each method has to be
97
+ # individually overridden.
98
+ if_timezone_offset(super) {|o,dt| dt.set_timezone_offset(o) }
99
+ end
100
+
101
+ # An overridden version of `DateTime#julian` that preserves the associated
102
+ # {TimezoneOffset}.
103
+ #
104
+ # @return [DateTime]
105
+ def julian
106
+ # super doesn't call #new_start on MRI, so each method has to be
107
+ # individually overridden.
108
+ if_timezone_offset(super) {|o,dt| dt.set_timezone_offset(o) }
109
+ end
110
+
111
+ # An overridden version of `DateTime#new_start` that preserves the
112
+ # associated {TimezoneOffset}.
113
+ #
114
+ # @return [DateTime]
115
+ def new_start(start = Date::ITALY)
116
+ if_timezone_offset(super) {|o,dt| dt.set_timezone_offset(o) }
117
+ end
118
+
119
+ # An overridden version of `DateTime#step` that clears the associated
120
+ # {TimezoneOffset} of the returned or yielded instances.
121
+ def step(limit, step = 1)
122
+ if block_given?
123
+ super {|dt| yield dt.clear_timezone_offset }
124
+ else
125
+ enum = super
126
+ enum.each {|dt| dt.clear_timezone_offset }
127
+ enum
128
+ end
129
+ end
130
+
131
+ # An overridden version of `DateTime#upto` that clears the associated
132
+ # {TimezoneOffset} of the returned or yielded instances.
133
+ def upto(max)
134
+ if block_given?
135
+ super {|dt| yield dt.clear_timezone_offset }
136
+ else
137
+ enum = super
138
+ enum.each {|dt| dt.clear_timezone_offset }
139
+ enum
140
+ end
141
+ end
142
+
143
+ protected
144
+
145
+ # Clears the associated {TimezoneOffset}.
146
+ #
147
+ # @return [DateTimeWithOffset] `self`.
148
+ def clear_timezone_offset
149
+ @timezone_offset = nil
150
+ self
151
+ end
152
+ end
153
+ end