tzinfo 1.2.11 → 2.0.0

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 (151) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.yardopts +3 -0
  4. data/CHANGES.md +469 -431
  5. data/LICENSE +13 -13
  6. data/README.md +368 -114
  7. data/lib/tzinfo/country.rb +131 -129
  8. data/lib/tzinfo/country_timezone.rb +70 -112
  9. data/lib/tzinfo/data_source.rb +389 -144
  10. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  11. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  12. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  13. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  14. data/lib/tzinfo/data_sources/ruby_data_source.rb +141 -0
  15. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  16. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  17. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +573 -0
  18. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +284 -0
  19. data/lib/tzinfo/data_sources.rb +8 -0
  20. data/lib/tzinfo/data_timezone.rb +33 -47
  21. data/lib/tzinfo/datetime_with_offset.rb +153 -0
  22. data/lib/tzinfo/format1/country_definer.rb +17 -0
  23. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  24. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  25. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  26. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  27. data/lib/tzinfo/format1.rb +10 -0
  28. data/lib/tzinfo/format2/country_definer.rb +68 -0
  29. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  30. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  31. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  32. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  33. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  34. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
  35. data/lib/tzinfo/format2.rb +10 -0
  36. data/lib/tzinfo/info_timezone.rb +26 -21
  37. data/lib/tzinfo/linked_timezone.rb +33 -52
  38. data/lib/tzinfo/offset_timezone_period.rb +42 -0
  39. data/lib/tzinfo/string_deduper.rb +118 -0
  40. data/lib/tzinfo/time_with_offset.rb +128 -0
  41. data/lib/tzinfo/timestamp.rb +548 -0
  42. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  43. data/lib/tzinfo/timezone.rb +979 -502
  44. data/lib/tzinfo/timezone_offset.rb +84 -74
  45. data/lib/tzinfo/timezone_period.rb +151 -217
  46. data/lib/tzinfo/timezone_proxy.rb +70 -79
  47. data/lib/tzinfo/timezone_transition.rb +77 -109
  48. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  49. data/lib/tzinfo/version.rb +7 -0
  50. data/lib/tzinfo/with_offset.rb +61 -0
  51. data/lib/tzinfo.rb +60 -40
  52. data.tar.gz.sig +0 -0
  53. metadata +51 -115
  54. metadata.gz.sig +2 -3
  55. data/Rakefile +0 -107
  56. data/lib/tzinfo/annual_rules.rb +0 -51
  57. data/lib/tzinfo/country_index_definition.rb +0 -31
  58. data/lib/tzinfo/country_info.rb +0 -42
  59. data/lib/tzinfo/data_timezone_info.rb +0 -55
  60. data/lib/tzinfo/linked_timezone_info.rb +0 -26
  61. data/lib/tzinfo/offset_rationals.rb +0 -77
  62. data/lib/tzinfo/posix_time_zone_parser.rb +0 -136
  63. data/lib/tzinfo/ruby_core_support.rb +0 -176
  64. data/lib/tzinfo/ruby_country_info.rb +0 -74
  65. data/lib/tzinfo/ruby_data_source.rb +0 -136
  66. data/lib/tzinfo/time_or_datetime.rb +0 -351
  67. data/lib/tzinfo/timezone_definition.rb +0 -36
  68. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  69. data/lib/tzinfo/timezone_info.rb +0 -30
  70. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  71. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  72. data/lib/tzinfo/transition_rule.rb +0 -325
  73. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  74. data/lib/tzinfo/zoneinfo_data_source.rb +0 -504
  75. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -516
  76. data/test/assets/payload.rb +0 -1
  77. data/test/tc_annual_rules.rb +0 -95
  78. data/test/tc_country.rb +0 -240
  79. data/test/tc_country_index_definition.rb +0 -69
  80. data/test/tc_country_info.rb +0 -16
  81. data/test/tc_country_timezone.rb +0 -173
  82. data/test/tc_data_source.rb +0 -218
  83. data/test/tc_data_timezone.rb +0 -99
  84. data/test/tc_data_timezone_info.rb +0 -18
  85. data/test/tc_info_timezone.rb +0 -34
  86. data/test/tc_linked_timezone.rb +0 -155
  87. data/test/tc_linked_timezone_info.rb +0 -23
  88. data/test/tc_offset_rationals.rb +0 -23
  89. data/test/tc_posix_time_zone_parser.rb +0 -261
  90. data/test/tc_ruby_core_support.rb +0 -168
  91. data/test/tc_ruby_country_info.rb +0 -110
  92. data/test/tc_ruby_data_source.rb +0 -175
  93. data/test/tc_time_or_datetime.rb +0 -674
  94. data/test/tc_timezone.rb +0 -1361
  95. data/test/tc_timezone_definition.rb +0 -113
  96. data/test/tc_timezone_index_definition.rb +0 -73
  97. data/test/tc_timezone_info.rb +0 -11
  98. data/test/tc_timezone_london.rb +0 -143
  99. data/test/tc_timezone_melbourne.rb +0 -142
  100. data/test/tc_timezone_new_york.rb +0 -142
  101. data/test/tc_timezone_offset.rb +0 -126
  102. data/test/tc_timezone_period.rb +0 -555
  103. data/test/tc_timezone_proxy.rb +0 -136
  104. data/test/tc_timezone_transition.rb +0 -366
  105. data/test/tc_timezone_transition_definition.rb +0 -295
  106. data/test/tc_timezone_utc.rb +0 -27
  107. data/test/tc_transition_data_timezone_info.rb +0 -433
  108. data/test/tc_transition_rule.rb +0 -663
  109. data/test/tc_zoneinfo_country_info.rb +0 -78
  110. data/test/tc_zoneinfo_data_source.rb +0 -1226
  111. data/test/tc_zoneinfo_timezone_info.rb +0 -2149
  112. data/test/test_utils.rb +0 -214
  113. data/test/ts_all.rb +0 -7
  114. data/test/ts_all_ruby.rb +0 -5
  115. data/test/ts_all_zoneinfo.rb +0 -9
  116. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  117. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -327
  118. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -230
  119. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  120. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  121. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  122. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  123. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -273
  124. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -198
  125. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -333
  126. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -277
  127. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -235
  128. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  129. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -940
  130. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -609
  131. data/test/tzinfo-data/tzinfo/data/version.rb +0 -20
  132. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  133. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  134. data/test/zoneinfo/America/New_York +0 -0
  135. data/test/zoneinfo/Australia/Melbourne +0 -0
  136. data/test/zoneinfo/EST +0 -0
  137. data/test/zoneinfo/Etc/UTC +0 -0
  138. data/test/zoneinfo/Europe/Amsterdam +0 -0
  139. data/test/zoneinfo/Europe/Andorra +0 -0
  140. data/test/zoneinfo/Europe/London +0 -0
  141. data/test/zoneinfo/Europe/Paris +0 -0
  142. data/test/zoneinfo/Europe/Prague +0 -0
  143. data/test/zoneinfo/Factory +0 -0
  144. data/test/zoneinfo/iso3166.tab +0 -274
  145. data/test/zoneinfo/leapseconds +0 -78
  146. data/test/zoneinfo/posix/Europe/London +0 -0
  147. data/test/zoneinfo/posixrules +0 -0
  148. data/test/zoneinfo/right/Europe/London +0 -0
  149. data/test/zoneinfo/zone.tab +0 -452
  150. data/test/zoneinfo/zone1970.tab +0 -384
  151. data/tzinfo.gemspec +0 -21
@@ -0,0 +1,284 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ module DataSources
6
+ # An {InvalidZoneinfoFile} exception is raised if an attempt is made to load
7
+ # an invalid zoneinfo file.
8
+ class InvalidZoneinfoFile < StandardError
9
+ end
10
+
11
+ # Reads compiled zoneinfo TZif (\0, 2 or 3) files.
12
+ class ZoneinfoReader #:nodoc:
13
+ # Initializes a new {ZoneinfoReader}.
14
+ #
15
+ # @param string_deduper [StringDeduper] a {StringDeduper} instance to use
16
+ # when deduping abbreviations.
17
+ def initialize(string_deduper)
18
+ @string_deduper = string_deduper
19
+ end
20
+
21
+ # Reads a zoneinfo structure from the given path. Returns either a
22
+ # {TimezoneOffset} that is constantly observed or an `Array`
23
+ # {TimezoneTransition}s.
24
+ #
25
+ # @param file_path [String] the path of a zoneinfo file.
26
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
27
+ # {TimezoneTransition}s.
28
+ # @raise [SecurityError] if safe mode is enabled and `file_path` is
29
+ # tainted.
30
+ # @raise [InvalidZoneinfoFile] if `file_path`` does not refer to a valid
31
+ # zoneinfo file.
32
+ def read(file_path)
33
+ File.open(file_path, 'rb') { |file| parse(file) }
34
+ end
35
+
36
+ private
37
+
38
+ # Translates an unsigned 32-bit integer (as returned by unpack) to signed
39
+ # 32-bit.
40
+ #
41
+ # @param long [Integer] an unsigned 32-bit integer.
42
+ # @return [Integer] {long} translated to signed 32-bit.
43
+ def make_signed_int32(long)
44
+ long >= 0x80000000 ? long - 0x100000000 : long
45
+ end
46
+
47
+ # Translates a pair of unsigned 32-bit integers (as returned by unpack,
48
+ # most significant first) to a signed 64-bit integer.
49
+ #
50
+ # @param high [Integer] the most significant 32-bits.
51
+ # @param low [Integer] the least significant 32-bits.
52
+ # @return [Integer] {high} and {low} combined and translated to signed
53
+ # 64-bit.
54
+ def make_signed_int64(high, low)
55
+ unsigned = (high << 32) | low
56
+ unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
57
+ end
58
+
59
+ # Reads the given number of bytes from the given file and checks that the
60
+ # correct number of bytes could be read.
61
+ #
62
+ # @param file [IO] the file to read from.
63
+ # @param bytes [Integer] the number of bytes to read.
64
+ # @return [String] the bytes that were read.
65
+ # @raise [InvalidZoneinfoFile] if the number of bytes available didn't
66
+ # match the number requested.
67
+ def check_read(file, bytes)
68
+ result = file.read(bytes)
69
+
70
+ unless result && result.length == bytes
71
+ raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
72
+ end
73
+
74
+ result
75
+ end
76
+
77
+ # Zoneinfo files don't include the offset from standard time (std_offset)
78
+ # for DST periods. Derive the base offset (base_utc_offset) where DST is
79
+ # observed from either the previous or next non-DST period.
80
+ #
81
+ # @param transitions [Array<Hash>] an `Array` of transition hashes.
82
+ # @param offsets [Array<Hash>] an `Array` of offset hashes.
83
+ # @return [Integer] the index of the offset to be used prior to the first
84
+ # transition.
85
+ def derive_offsets(transitions, offsets)
86
+ # The first non-DST offset (if there is one) is the offset observed
87
+ # before the first transition. Fall back to the first DST offset if
88
+ # there are no non-DST offsets.
89
+ first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
90
+ first_offset_index = first_non_dst_offset_index || 0
91
+ return first_offset_index if transitions.empty?
92
+
93
+ # Determine the base_utc_offset of the next non-dst offset at each transition.
94
+ base_utc_offset_from_next = nil
95
+
96
+ transitions.reverse_each do |transition|
97
+ offset = offsets[transition[:offset]]
98
+ if offset[:is_dst]
99
+ transition[:base_utc_offset_from_next] = base_utc_offset_from_next if base_utc_offset_from_next
100
+ else
101
+ base_utc_offset_from_next = offset[:observed_utc_offset]
102
+ end
103
+ end
104
+
105
+ base_utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:observed_utc_offset] : nil
106
+ defined_offsets = {}
107
+
108
+ transitions.each do |transition|
109
+ offset_index = transition[:offset]
110
+ offset = offsets[offset_index]
111
+ observed_utc_offset = offset[:observed_utc_offset]
112
+
113
+ if offset[:is_dst]
114
+ base_utc_offset_from_next = transition[:base_utc_offset_from_next]
115
+
116
+ difference_to_previous = (observed_utc_offset - (base_utc_offset_from_previous || observed_utc_offset)).abs
117
+ difference_to_next = (observed_utc_offset - (base_utc_offset_from_next || observed_utc_offset)).abs
118
+
119
+ base_utc_offset = if difference_to_previous == 3600
120
+ base_utc_offset_from_previous
121
+ elsif difference_to_next == 3600
122
+ base_utc_offset_from_next
123
+ elsif difference_to_previous > 0 && difference_to_next > 0
124
+ difference_to_previous < difference_to_next ? base_utc_offset_from_previous : base_utc_offset_from_next
125
+ elsif difference_to_previous > 0
126
+ base_utc_offset_from_previous
127
+ elsif difference_to_next > 0
128
+ base_utc_offset_from_next
129
+ else
130
+ # No difference, assume a 1 hour offset from standard time.
131
+ observed_utc_offset - 3600
132
+ end
133
+
134
+ if !offset[:base_utc_offset]
135
+ offset[:base_utc_offset] = base_utc_offset
136
+ defined_offsets[offset] = offset_index
137
+ elsif offset[:base_utc_offset] != base_utc_offset
138
+ # An earlier transition has already derived a different
139
+ # base_utc_offset. Define a new offset or reuse an existing identically
140
+ # defined offset.
141
+ new_offset = offset.dup
142
+ new_offset[:base_utc_offset] = base_utc_offset
143
+
144
+ offset_index = defined_offsets[new_offset]
145
+
146
+ unless offset_index
147
+ offsets << new_offset
148
+ offset_index = offsets.length - 1
149
+ defined_offsets[new_offset] = offset_index
150
+ end
151
+
152
+ transition[:offset] = offset_index
153
+ end
154
+ else
155
+ base_utc_offset_from_previous = observed_utc_offset
156
+ end
157
+ end
158
+
159
+ first_offset_index
160
+ end
161
+
162
+ # Parses a zoneinfo file and returns either a {TimezoneOffset} that is
163
+ # constantly observed or an `Array` of {TimezoneTransition}s.
164
+ #
165
+ # @param file [IO] the file to be read.
166
+ # @return [Object] either a {TimezoneOffset} or an `Array` of
167
+ # {TimezoneTransition}s.
168
+ # @raise [InvalidZoneinfoFile] if the file is not a valid zoneinfo file.
169
+ def parse(file)
170
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
171
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
172
+
173
+ if magic != 'TZif'
174
+ raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
175
+ end
176
+
177
+ if version == '2' || version == '3'
178
+ # Skip the first 32-bit section and read the header of the second 64-bit section
179
+ file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisgmtcnt + ttisstdcnt, IO::SEEK_CUR)
180
+
181
+ prev_version = version
182
+
183
+ magic, version, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
184
+ check_read(file, 44).unpack('a4 a x15 NNNNNN')
185
+
186
+ unless magic == 'TZif' && (version == prev_version)
187
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
188
+ end
189
+
190
+ using_64bit = true
191
+ elsif version != '3' && version != '2' && version != "\0"
192
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
193
+ else
194
+ using_64bit = false
195
+ end
196
+
197
+ unless leapcnt == 0
198
+ raise InvalidZoneinfoFile, "The file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
199
+ end
200
+
201
+ transitions = if using_64bit
202
+ timecnt.times.map do |i|
203
+ high, low = check_read(file, 8).unpack('NN'.freeze)
204
+ transition_time = make_signed_int64(high, low)
205
+ {at: transition_time}
206
+ end
207
+ else
208
+ timecnt.times.map do |i|
209
+ transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
210
+ {at: transition_time}
211
+ end
212
+ end
213
+
214
+ check_read(file, timecnt).unpack('C*'.freeze).each_with_index do |localtime_type, i|
215
+ raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'." if localtime_type >= typecnt
216
+ transitions[i][:offset] = localtime_type
217
+ end
218
+
219
+ offsets = typecnt.times.map do |i|
220
+ gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
221
+ gmtoff = make_signed_int32(gmtoff)
222
+ isdst = isdst == 1
223
+ {observed_utc_offset: gmtoff, is_dst: isdst, abbr_index: abbrind}
224
+ end
225
+
226
+ abbrev = check_read(file, charcnt)
227
+
228
+ # Derive the offsets from standard time (std_offset).
229
+ first_offset_index = derive_offsets(transitions, offsets)
230
+
231
+ offsets = offsets.map do |o|
232
+ observed_utc_offset = o[:observed_utc_offset]
233
+ base_utc_offset = o[:base_utc_offset]
234
+
235
+ if base_utc_offset
236
+ # DST offset with base_utc_offset derived by derive_offsets.
237
+ std_offset = observed_utc_offset - base_utc_offset
238
+ elsif o[:is_dst]
239
+ # DST offset unreferenced by a transition (offset in use before the
240
+ # first transition). No derived base UTC offset, so assume 1 hour
241
+ # DST.
242
+ base_utc_offset = observed_utc_offset - 3600
243
+ std_offset = 3600
244
+ else
245
+ # Non-DST offset.
246
+ base_utc_offset = observed_utc_offset
247
+ std_offset = 0
248
+ end
249
+
250
+ abbrev_start = o[:abbr_index]
251
+ raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'." unless abbrev_start < abbrev.length
252
+
253
+ abbrev_end = abbrev.index("\0", abbrev_start)
254
+ raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'." unless abbrev_end
255
+
256
+ abbr = @string_deduper.dedupe(abbrev[abbrev_start...abbrev_end].force_encoding(Encoding::UTF_8).untaint)
257
+
258
+ TimezoneOffset.new(base_utc_offset, std_offset, abbr)
259
+ end
260
+
261
+ first_offset = offsets[first_offset_index]
262
+
263
+
264
+ if transitions.empty?
265
+ first_offset
266
+ else
267
+ previous_offset = first_offset
268
+ previous_at = nil
269
+
270
+ transitions.map do |t|
271
+ offset = offsets[t[:offset]]
272
+ at = t[:at]
273
+ 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
274
+ tt = TimezoneTransition.new(offset, previous_offset, at)
275
+ previous_offset = offset
276
+ previous_at = at
277
+ tt
278
+ end
279
+ end
280
+ end
281
+ end
282
+ private_constant :ZoneinfoReader
283
+ end
284
+ 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
@@ -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
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+
3
+ module TZInfo
4
+ module Format1
5
+ # Instances of {Format1::CountryDefiner} are yielded to the format 1 version
6
+ # of `TZInfo::Data::Indexes::Countries` by {CountryIndexDefinition} to allow
7
+ # the zones of a country to be specified.
8
+ #
9
+ # @private
10
+ class CountryDefiner < Format2::CountryDefiner #:nodoc:
11
+ # Initializes a new {CountryDefiner}.
12
+ def initialize(identifier_deduper, description_deduper)
13
+ super(nil, identifier_deduper, description_deduper)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: UTF-8
2
+
3
+ module TZInfo
4
+ module Format1
5
+ # The format 1 TZInfo::Data country index file includes
6
+ # {Format1::CountryIndexDefinition}, which provides a
7
+ # {CountryIndexDefinition::ClassMethods#country country} method used to
8
+ # define each country in the index.
9
+ #
10
+ # @private
11
+ module CountryIndexDefinition #:nodoc:
12
+ # Adds class methods to the includee and initializes class instance
13
+ # variables.
14
+ #
15
+ # @param base [Module] the includee.
16
+ def self.append_features(base)
17
+ super
18
+ base.extend(ClassMethods)
19
+ base.instance_eval { @countries = {} }
20
+ end
21
+
22
+ # Class methods for inclusion.
23
+ #
24
+ # @private
25
+ module ClassMethods #:nodoc:
26
+ # @return [Hash<String, DataSources::CountryInfo>] a frozen `Hash`
27
+ # of all the countries that have been defined in the index keyed by
28
+ # their codes.
29
+ def countries
30
+ @description_deduper = nil
31
+ @countries.freeze
32
+ end
33
+
34
+ private
35
+
36
+ # Defines a country with an ISO 3166-1 alpha-2 country code and name.
37
+ #
38
+ # @param code [String] the ISO 3166-1 alpha-2 country code.
39
+ # @param name [String] the name of the country.
40
+ # @yield [definer] (optional) to obtain the time zones for the country.
41
+ # @yieldparam definer [CountryDefiner] a {CountryDefiner} instance.
42
+ def country(code, name)
43
+ @description_deduper ||= StringDeduper.new
44
+
45
+ zones = if block_given?
46
+ definer = CountryDefiner.new(StringDeduper.global, @description_deduper)
47
+ yield definer
48
+ definer.timezones
49
+ else
50
+ []
51
+ end
52
+
53
+ @countries[code.freeze] = DataSources::CountryInfo.new(code, name, zones)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # Alias used by TZInfo::Data format1 releases.
60
+ #
61
+ # @private
62
+ CountryIndexDefinition = Format1::CountryIndexDefinition #:nodoc:
63
+ private_constant :CountryIndexDefinition
64
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ module Format1
6
+ # Instances of {Format1::TimezoneDefiner} are yielded to TZInfo::Data
7
+ # format 1 modules by {TimezoneDefinition} to allow the offsets and
8
+ # transitions of the time zone to be specified.
9
+ #
10
+ # @private
11
+ class TimezoneDefiner < Format2::TimezoneDefiner #:nodoc:
12
+ undef_method :subsequent_rules
13
+
14
+ # Defines an offset.
15
+ #
16
+ # @param id [Symbol] an arbitrary value used identify the offset in
17
+ # subsequent calls to transition. It must be unique.
18
+ # @param utc_offset [Integer] the base offset from UTC of the zone in
19
+ # seconds. This does not include daylight savings time.
20
+ # @param std_offset [Integer] the daylight savings offset from the base
21
+ # offset in seconds. Typically either 0 or 3600.
22
+ # @param abbreviation [Symbol] an abbreviation for the offset, for
23
+ # example, `:EST` or `:EDT`.
24
+ # @raise [ArgumentError] if another offset has already been defined with
25
+ # the given id.
26
+ def offset(id, utc_offset, std_offset, abbreviation)
27
+ super(id, utc_offset, std_offset, abbreviation.to_s)
28
+ end
29
+
30
+ # Defines a transition to a given offset.
31
+ #
32
+ # Transitions must be defined in increasing time order.
33
+ #
34
+ # @param year [Integer] the UTC year in which the transition occurs. Used
35
+ # in earlier versions of TZInfo, but now ignored.
36
+ # @param month [Integer] the UTC month in which the transition occurs.
37
+ # Used in earlier versions of TZInfo, but now ignored.
38
+ # @param offset_id [Symbol] references the id of a previously defined
39
+ # offset (see #offset).
40
+ # @param timestamp_value [Integer] the time the transition occurs as an
41
+ # Integer number of seconds since 1970-01-01 00:00:00 UTC ignoring leap
42
+ # seconds (i.e. each day is treated as if it were 86,400 seconds long).
43
+ # @param datetime_numerator [Integer] the time of the transition as the
44
+ # numerator of the `Rational` returned by `DateTime#ajd`. Used in
45
+ # earlier versions of TZInfo, but now ignored.
46
+ # @param datetime_denominator [Integer] the time of the transition as the
47
+ # denominator of the `Rational` returned by `DateTime#ajd`. Used in
48
+ # earlier versions of TZInfo, but now ignored.
49
+ # @raise [ArgumentError] if `offset_id` does not reference a defined
50
+ # offset.
51
+ # @raise [ArgumentError] if `timestamp_value` is not greater than the
52
+ # `timestamp_value` of the previously defined transition.
53
+ # @raise [ArgumentError] if `datetime_numerator` is specified, but
54
+ # `datetime_denominator` is not. In older versions of TZInfo, it was
55
+ # possible to define a transition with the `DateTime` numerator as the
56
+ # 4th parameter and the denominator as the 5th parameter. This style of
57
+ # definition is not used in released versions of TZInfo::Data.
58
+ def transition(year, month, offset_id, timestamp_value, datetime_numerator = nil, datetime_denominator = nil)
59
+ raise ArgumentError, 'DateTime-only transitions are not supported' if datetime_numerator && !datetime_denominator
60
+ super(offset_id, timestamp_value)
61
+ end
62
+ end
63
+ end
64
+ end