tzinfo 1.2.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,548 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ # A time represented as an `Integer` number of seconds since 1970-01-01
6
+ # 00:00:00 UTC (ignoring leap seconds), the fraction through the second
7
+ # (sub_second as a `Rational`) and an optional UTC offset. Like Ruby's `Time`
8
+ # class, {Timestamp} can distinguish between a local time with a zero offset
9
+ # and a time specified explicitly as UTC.
10
+ class Timestamp
11
+ include Comparable
12
+
13
+ # The Unix epoch (1970-01-01 00:00:00 UTC) as a chronological Julian day
14
+ # number.
15
+ JD_EPOCH = 2440588
16
+ private_constant :JD_EPOCH
17
+
18
+ class << self
19
+ # Returns a new {Timestamp} representing the (Gregorian calendar) date and
20
+ # time specified by the supplied parameters.
21
+ #
22
+ # If `utc_offset` is `nil`, `:utc` or 0, the date and time parameters will
23
+ # be interpreted as representing a UTC date and time. Otherwise the date
24
+ # and time parameters will be interpreted as a local date and time with
25
+ # the given offset.
26
+ #
27
+ # @param year [Integer] the year.
28
+ # @param month [Integer] the month (1-12).
29
+ # @param day [Integer] the day of the month (1-31).
30
+ # @param hour [Integer] the hour (0-23).
31
+ # @param minute [Integer] the minute (0-59).
32
+ # @param second [Integer] the second (0-59).
33
+ # @param sub_second [Numeric] the fractional part of the second as either
34
+ # a `Rational` that is greater than or equal to 0 and less than 1, or
35
+ # the `Integer` 0.
36
+ # @param utc_offset [Object] either `nil` for a {Timestamp} without a
37
+ # specified offset, an offset from UTC specified as an `Integer` number
38
+ # of seconds or the `Symbol` `:utc`).
39
+ # @return [Timestamp] a new {Timestamp} representing the specified
40
+ # (Gregorian calendar) date and time.
41
+ # @raise [ArgumentError] if either of `year`, `month`, `day`, `hour`,
42
+ # `minute`, or `second` is not an `Integer`.
43
+ # @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
44
+ # `Integer` 0.
45
+ # @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer`
46
+ # and not the `Symbol` `:utc`.
47
+ # @raise [RangeError] if `month` is not between 1 and 12.
48
+ # @raise [RangeError] if `day` is not between 1 and 31.
49
+ # @raise [RangeError] if `hour` is not between 0 and 23.
50
+ # @raise [RangeError] if `minute` is not between 0 and 59.
51
+ # @raise [RangeError] if `second` is not between 0 and 59.
52
+ # @raise [RangeError] if `sub_second` is a `Rational` but that is less
53
+ # than 0 or greater than or equal to 1.
54
+ def create(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, sub_second = 0, utc_offset = nil)
55
+ raise ArgumentError, 'year must be an Integer' unless year.kind_of?(Integer)
56
+ raise ArgumentError, 'month must be an Integer' unless month.kind_of?(Integer)
57
+ raise ArgumentError, 'day must be an Integer' unless day.kind_of?(Integer)
58
+ raise ArgumentError, 'hour must be an Integer' unless hour.kind_of?(Integer)
59
+ raise ArgumentError, 'minute must be an Integer' unless minute.kind_of?(Integer)
60
+ raise ArgumentError, 'second must be an Integer' unless second.kind_of?(Integer)
61
+ raise RangeError, 'month must be between 1 and 12' if month < 1 || month > 12
62
+ raise RangeError, 'day must be between 1 and 31' if day < 1 || day > 31
63
+ raise RangeError, 'hour must be between 0 and 23' if hour < 0 || hour > 23
64
+ raise RangeError, 'minute must be between 0 and 59' if minute < 0 || minute > 59
65
+ raise RangeError, 'second must be between 0 and 59' if second < 0 || second > 59
66
+
67
+ # Based on days_from_civil from https://howardhinnant.github.io/date_algorithms.html#days_from_civil
68
+ after_february = month > 2
69
+ year -= 1 unless after_february
70
+ era = year / 400
71
+ year_of_era = year - era * 400
72
+ day_of_year = (153 * (month + (after_february ? -3 : 9)) + 2) / 5 + day - 1
73
+ day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year
74
+ days_since_epoch = era * 146097 + day_of_era - 719468
75
+ value = ((days_since_epoch * 24 + hour) * 60 + minute) * 60 + second
76
+ value -= utc_offset if utc_offset.kind_of?(Integer)
77
+
78
+ new(value, sub_second, utc_offset)
79
+ end
80
+
81
+ # When used without a block, returns a {Timestamp} representation of a
82
+ # given `Time`, `DateTime` or {Timestamp}.
83
+ #
84
+ # When called with a block, the {Timestamp} representation of `value` is
85
+ # passed to the block. The block must then return a {Timestamp}, which
86
+ # will be converted back to the type of the initial value. If the initial
87
+ # value was a {Timestamp}, the block result will just be returned.
88
+ #
89
+ # The UTC offset of `value` can either be preserved (the {Timestamp}
90
+ # representation will have the same UTC offset as `value`), ignored (the
91
+ # {Timestamp} representation will have no defined UTC offset), or treated
92
+ # as though it were UTC (the {Timestamp} representation will have a
93
+ # {utc_offset} of 0 and {utc?} will return `true`).
94
+ #
95
+ # @param value [Object] a `Time`, `DateTime` or {Timestamp}.
96
+ # @param offset [Symbol] either `:preserve` to preserve the offset of
97
+ # `value`, `:ignore` to ignore the offset of `value` and create a
98
+ # {Timestamp} with an unspecified offset, or `:treat_as_utc` to treat
99
+ # the offset of `value` as though it were UTC and create a UTC
100
+ # {Timestamp}.
101
+ # @yield [timestamp] if a block is provided, the {Timestamp}
102
+ # representation is passed to the block.
103
+ # @yieldparam timestamp [Timestamp] the {Timestamp} representation of
104
+ # `value`.
105
+ # @yieldreturn [Timestamp] a {Timestamp} to be converted back to the type
106
+ # of `value`.
107
+ # @return [Object] if called without a block, the {Timestamp}
108
+ # representation of `value`, otherwise the result of the block,
109
+ # converted back to the type of `value`.
110
+ def for(value, offset = :preserve)
111
+ raise ArgumentError, 'value must be specified' unless value
112
+
113
+ case offset
114
+ when :ignore
115
+ ignore_offset = true
116
+ target_utc_offset = nil
117
+ when :treat_as_utc
118
+ ignore_offset = true
119
+ target_utc_offset = :utc
120
+ when :preserve
121
+ ignore_offset = false
122
+ target_utc_offset = nil
123
+ else
124
+ raise ArgumentError, 'offset must be :preserve, :ignore or :treat_as_utc'
125
+ end
126
+
127
+ time_like = false
128
+ timestamp = case value
129
+ when Time
130
+ for_time(value, ignore_offset, target_utc_offset)
131
+ when DateTime
132
+ for_datetime(value, ignore_offset, target_utc_offset)
133
+ when Timestamp
134
+ for_timestamp(value, ignore_offset, target_utc_offset)
135
+ else
136
+ raise ArgumentError, "#{value.class} values are not supported" unless is_time_like?(value)
137
+ time_like = true
138
+ for_time_like(value, ignore_offset, target_utc_offset)
139
+ end
140
+
141
+ if block_given?
142
+ result = yield timestamp
143
+ raise ArgumentError, 'block must return a Timestamp' unless result.kind_of?(Timestamp)
144
+
145
+ case value
146
+ when Time
147
+ result.to_time
148
+ when DateTime
149
+ result.to_datetime
150
+ else # A Time-like value or a Timestamp
151
+ time_like ? result.to_time : result
152
+ end
153
+ else
154
+ timestamp
155
+ end
156
+ end
157
+
158
+ # Creates a new UTC {Timestamp}.
159
+ #
160
+ # @param value [Integer] the number of seconds since 1970-01-01 00:00:00
161
+ # UTC ignoring leap seconds.
162
+ # @param sub_second [Numeric] the fractional part of the second as either
163
+ # a `Rational` that is greater than or equal to 0 and less than 1, or
164
+ # the `Integer` 0.
165
+ # @raise [ArgumentError] if `value` is not an `Integer`.
166
+ # @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
167
+ # `Integer` 0.
168
+ # @raise [RangeError] if `sub_second` is a `Rational` but that is less
169
+ # than 0 or greater than or equal to 1.
170
+ def utc(value, sub_second = 0)
171
+ new(value, sub_second, :utc)
172
+ end
173
+
174
+ private
175
+
176
+ # Constructs a new instance of `self` (i.e. {Timestamp} or a subclass of
177
+ # {Timestamp}) without validating the parameters. This method is used
178
+ # internally within {Timestamp} to avoid the overhead of checking
179
+ # parameters.
180
+ #
181
+ # @param value [Integer] the number of seconds since 1970-01-01 00:00:00
182
+ # UTC ignoring leap seconds.
183
+ # @param sub_second [Numeric] the fractional part of the second as either
184
+ # a `Rational` that is greater than or equal to 0 and less than 1, or
185
+ # the `Integer` 0.
186
+ # @param utc_offset [Object] either `nil` for a {Timestamp} without a
187
+ # specified offset, an offset from UTC specified as an `Integer` number
188
+ # of seconds or the `Symbol` `:utc`).
189
+ # @return [Timestamp] a new instance of `self`.
190
+ def new!(value, sub_second = 0, utc_offset = nil)
191
+ result = allocate
192
+ result.send(:initialize!, value, sub_second, utc_offset)
193
+ result
194
+ end
195
+
196
+ # Creates a {Timestamp} that represents a given `Time`, optionally
197
+ # ignoring the offset.
198
+ #
199
+ # @param time [Time] a `Time`.
200
+ # @param ignore_offset [Boolean] whether to ignore the offset of `time`.
201
+ # @param target_utc_offset [Object] if `ignore_offset` is `true`, the UTC
202
+ # offset of the result (`:utc`, `nil` or an `Integer`).
203
+ # @return [Timestamp] the {Timestamp} representation of `time`.
204
+ def for_time(time, ignore_offset, target_utc_offset)
205
+ value = time.to_i
206
+ sub_second = time.subsec
207
+
208
+ if ignore_offset
209
+ utc_offset = target_utc_offset
210
+ value += time.utc_offset
211
+ elsif time.utc?
212
+ utc_offset = :utc
213
+ else
214
+ utc_offset = time.utc_offset
215
+ end
216
+
217
+ new!(value, sub_second, utc_offset)
218
+ end
219
+
220
+ # Creates a {Timestamp} that represents a given `DateTime`, optionally
221
+ # ignoring the offset.
222
+ #
223
+ # @param datetime [DateTime] a `DateTime`.
224
+ # @param ignore_offset [Boolean] whether to ignore the offset of
225
+ # `datetime`.
226
+ # @param target_utc_offset [Object] if `ignore_offset` is `true`, the UTC
227
+ # offset of the result (`:utc`, `nil` or an `Integer`).
228
+ # @return [Timestamp] the {Timestamp} representation of `datetime`.
229
+ def for_datetime(datetime, ignore_offset, target_utc_offset)
230
+ value = (datetime.jd - JD_EPOCH) * 86400 + datetime.sec + datetime.min * 60 + datetime.hour * 3600
231
+ sub_second = datetime.sec_fraction
232
+
233
+ if ignore_offset
234
+ utc_offset = target_utc_offset
235
+ else
236
+ utc_offset = (datetime.offset * 86400).to_i
237
+ value -= utc_offset
238
+ end
239
+
240
+ new!(value, sub_second, utc_offset)
241
+ end
242
+
243
+ # Returns a {Timestamp} that represents another {Timestamp}, optionally
244
+ # ignoring the offset. If the result would be identical to `value`, the
245
+ # same instance is returned. If the passed in value is an instance of a
246
+ # subclass of {Timestamp}, then a new {Timestamp} will always be returned.
247
+ #
248
+ # @param timestamp [Timestamp] a {Timestamp}.
249
+ # @param ignore_offset [Boolean] whether to ignore the offset of
250
+ # `timestamp`.
251
+ # @param target_utc_offset [Object] if `ignore_offset` is `true`, the UTC
252
+ # offset of the result (`:utc`, `nil` or an `Integer`).
253
+ # @return [Timestamp] a [Timestamp] representation of `timestamp`.
254
+ def for_timestamp(timestamp, ignore_offset, target_utc_offset)
255
+ if ignore_offset
256
+ if target_utc_offset
257
+ unless target_utc_offset == :utc && timestamp.utc? || timestamp.utc_offset == target_utc_offset
258
+ return new!(timestamp.value + (timestamp.utc_offset || 0), timestamp.sub_second, target_utc_offset)
259
+ end
260
+ elsif timestamp.utc_offset
261
+ return new!(timestamp.value + timestamp.utc_offset, timestamp.sub_second)
262
+ end
263
+ end
264
+
265
+ unless timestamp.instance_of?(Timestamp)
266
+ # timestamp is identical in value, sub_second and utc_offset but is a
267
+ # subclass (i.e. TimestampWithOffset). Return a new Timestamp
268
+ # instance.
269
+ return new!(timestamp.value, timestamp.sub_second, timestamp.utc? ? :utc : timestamp.utc_offset)
270
+ end
271
+
272
+ timestamp
273
+ end
274
+
275
+ # Determines if an object is like a `Time` (for the purposes of converting
276
+ # to a {Timestamp} with {for}), responding to `to_i` and `subsec`.
277
+ #
278
+ # @param value [Object] an object to test.
279
+ # @return [Boolean] `true` if the object is `Time`-like, otherwise
280
+ # `false`.
281
+ def is_time_like?(value)
282
+ value.respond_to?(:to_i) && value.respond_to?(:subsec)
283
+ end
284
+
285
+ # Creates a {Timestamp} that represents a given `Time`-like object,
286
+ # optionally ignoring the offset (if the `time_like` responds to
287
+ # `utc_offset`).
288
+ #
289
+ # @param time_like [Object] a `Time`-like object.
290
+ # @param ignore_offset [Boolean] whether to ignore the offset of `time`.
291
+ # @param target_utc_offset [Object] if `ignore_offset` is `true`, the UTC
292
+ # offset of the result (`:utc`, `nil` or an `Integer`).
293
+ # @return [Timestamp] the {Timestamp} representation of `time_like`.
294
+ def for_time_like(time_like, ignore_offset, target_utc_offset)
295
+ value = time_like.to_i
296
+ sub_second = time_like.subsec.to_r
297
+
298
+ if ignore_offset
299
+ utc_offset = target_utc_offset
300
+ value += time_like.utc_offset.to_i if time_like.respond_to?(:utc_offset)
301
+ elsif time_like.respond_to?(:utc_offset)
302
+ utc_offset = time_like.utc_offset.to_i
303
+ else
304
+ utc_offset = 0
305
+ end
306
+
307
+ new(value, sub_second, utc_offset)
308
+ end
309
+ end
310
+
311
+ # @return [Integer] the number of seconds since 1970-01-01 00:00:00 UTC
312
+ # ignoring leap seconds (i.e. each day is treated as if it were 86,400
313
+ # seconds long).
314
+ attr_reader :value
315
+
316
+ # @return [Numeric] the fraction of a second elapsed since timestamp as
317
+ # either a `Rational` or the `Integer` 0. Always greater than or equal to
318
+ # 0 and less than 1.
319
+ attr_reader :sub_second
320
+
321
+ # @return [Integer] the offset from UTC in seconds or `nil` if the
322
+ # {Timestamp} doesn't have a specified offset.
323
+ attr_reader :utc_offset
324
+
325
+ # Initializes a new {Timestamp}.
326
+ #
327
+ # @param value [Integer] the number of seconds since 1970-01-01 00:00:00 UTC
328
+ # ignoring leap seconds.
329
+ # @param sub_second [Numeric] the fractional part of the second as either a
330
+ # `Rational` that is greater than or equal to 0 and less than 1, or
331
+ # the `Integer` 0.
332
+ # @param utc_offset [Object] either `nil` for a {Timestamp} without a
333
+ # specified offset, an offset from UTC specified as an `Integer` number of
334
+ # seconds or the `Symbol` `:utc`).
335
+ # @raise [ArgumentError] if `value` is not an `Integer`.
336
+ # @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
337
+ # `Integer` 0.
338
+ # @raise [RangeError] if `sub_second` is a `Rational` but that is less
339
+ # than 0 or greater than or equal to 1.
340
+ # @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer` and
341
+ # not the `Symbol` `:utc`.
342
+ def initialize(value, sub_second = 0, utc_offset = nil)
343
+ raise ArgumentError, 'value must be an Integer' unless value.kind_of?(Integer)
344
+ raise ArgumentError, 'sub_second must be a Rational or the Integer 0' unless (sub_second.kind_of?(Integer) && sub_second == 0) || sub_second.kind_of?(Rational)
345
+ raise RangeError, 'sub_second must be >= 0 and < 1' if sub_second < 0 || sub_second >= 1
346
+ raise ArgumentError, 'utc_offset must be an Integer, :utc or nil' if utc_offset && utc_offset != :utc && !utc_offset.kind_of?(Integer)
347
+ initialize!(value, sub_second, utc_offset)
348
+ end
349
+
350
+ # @return [Boolean] `true` if this {Timestamp} represents UTC, `false` if
351
+ # the {Timestamp} wasn't specified as UTC or `nil` if the {Timestamp} has
352
+ # no specified offset.
353
+ def utc?
354
+ @utc
355
+ end
356
+
357
+ # Adds a number of seconds to the {Timestamp} value, setting the UTC offset
358
+ # of the result.
359
+ #
360
+ # @param seconds [Integer] the number of seconds to be added.
361
+ # @param utc_offset [Object] either `nil` for a {Timestamp} without a
362
+ # specified offset, an offset from UTC specified as an `Integer` number of
363
+ # seconds or the `Symbol` `:utc`).
364
+ # @return [Timestamp] the result of adding `seconds` to the
365
+ # {Timestamp} value as a new {Timestamp} instance with the chosen
366
+ # `utc_offset`.
367
+ # @raise [ArgumentError] if `seconds` is not an `Integer`.
368
+ # @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer` and
369
+ # not the `Symbol` `:utc`.
370
+ def add_and_set_utc_offset(seconds, utc_offset)
371
+ raise ArgumentError, 'seconds must be an Integer' unless seconds.kind_of?(Integer)
372
+ raise ArgumentError, 'utc_offset must be an Integer, :utc or nil' if utc_offset && utc_offset != :utc && !utc_offset.kind_of?(Integer)
373
+ return self if seconds == 0 && utc_offset == (@utc ? :utc : @utc_offset)
374
+ Timestamp.send(:new!, @value + seconds, @sub_second, utc_offset)
375
+ end
376
+
377
+ # @return [Timestamp] a UTC {Timestamp} equivalent to this instance. Returns
378
+ # `self` if {#utc? self.utc?} is `true`.
379
+ def utc
380
+ return self if @utc
381
+ Timestamp.send(:new!, @value, @sub_second, :utc)
382
+ end
383
+
384
+ # Converts this {Timestamp} to a `Time`.
385
+ #
386
+ # @return [Time] a `Time` representation of this {Timestamp}. If the UTC
387
+ # offset of this {Timestamp} is not specified, a UTC `Time` will be
388
+ # returned.
389
+ def to_time
390
+ time = new_time
391
+
392
+ if @utc_offset && !@utc
393
+ time.localtime(@utc_offset)
394
+ else
395
+ time.utc
396
+ end
397
+ end
398
+
399
+ # Converts this {Timestamp} to a `DateTime`.
400
+ #
401
+ # @return [DateTime] a DateTime representation of this {Timestamp}. If the
402
+ # UTC offset of this {Timestamp} is not specified, a UTC `DateTime` will
403
+ # be returned.
404
+ def to_datetime
405
+ new_datetime
406
+ end
407
+
408
+ # Converts this {Timestamp} to an `Integer` number of seconds since
409
+ # 1970-01-01 00:00:00 UTC (ignoring leap seconds).
410
+ #
411
+ # @return [Integer] an Integer representation of this {Timestamp} (the
412
+ # number of seconds since 1970-01-01 00:00:00 UTC ignoring leap seconds).
413
+ def to_i
414
+ value
415
+ end
416
+
417
+ # Formats this {Timestamp} according to the directives in the given format
418
+ # string.
419
+ #
420
+ # @param format [String] the format string. Please refer to `Time#strftime`
421
+ # for a list of supported format directives.
422
+ # @return [String] the formatted {Timestamp}.
423
+ # @raise [ArgumentError] if `format` is not specified.
424
+ def strftime(format)
425
+ raise ArgumentError, 'format must be specified' unless format
426
+ to_time.strftime(format)
427
+ end
428
+
429
+ # @return [String] a `String` representation of this {Timestamp}.
430
+ def to_s
431
+ return value_and_sub_second_to_s unless @utc_offset
432
+ return "#{value_and_sub_second_to_s} UTC" if @utc
433
+
434
+ sign = @utc_offset >= 0 ? '+' : '-'
435
+ min, sec = @utc_offset.abs.divmod(60)
436
+ hour, min = min.divmod(60)
437
+
438
+ "#{value_and_sub_second_to_s(@utc_offset)} #{sign}#{'%02d' % hour}:#{'%02d' % min}#{sec > 0 ? ':%02d' % sec : nil}#{@utc_offset != 0 ? " (#{value_and_sub_second_to_s} UTC)" : nil}"
439
+ end
440
+
441
+ # Compares this {Timestamp} with another.
442
+ #
443
+ # {Timestamp} instances without a defined UTC offset are not comparable with
444
+ # {Timestamp} instances that have a defined UTC offset.
445
+ #
446
+ # @param t [Timestamp] the {Timestamp} to compare this instance with.
447
+ # @return [Integer] -1, 0 or 1 depending if this instance is earlier, equal
448
+ # or later than `t` respectively. Returns `nil` when comparing a
449
+ # {Timestamp} that does not have a defined UTC offset with a {Timestamp}
450
+ # that does have a defined UTC offset. Returns `nil` if `t` is not a
451
+ # {Timestamp}.
452
+ def <=>(t)
453
+ return nil unless t.kind_of?(Timestamp)
454
+ return nil if utc_offset && !t.utc_offset
455
+ return nil if !utc_offset && t.utc_offset
456
+
457
+ result = value <=> t.value
458
+ result = sub_second <=> t.sub_second if result == 0
459
+ result
460
+ end
461
+
462
+ alias eql? ==
463
+
464
+ # @return [Integer] a hash based on the value, sub-second and whether there
465
+ # is a defined UTC offset.
466
+ def hash
467
+ [@value, @sub_second, !!@utc_offset].hash
468
+ end
469
+
470
+ # @return [String] the internal object state as a programmer-readable
471
+ # `String`.
472
+ def inspect
473
+ "#<#{self.class}: @value=#{@value}, @sub_second=#{@sub_second}, @utc_offset=#{@utc_offset.inspect}, @utc=#{@utc.inspect}>"
474
+ end
475
+
476
+ protected
477
+
478
+ # Creates a new instance of a `Time` or `Time`-like class matching the
479
+ # {value} and {sub_second} of this {Timestamp}, but not setting the offset.
480
+ #
481
+ # @param klass [Class] the class to instantiate.
482
+ #
483
+ # @private
484
+ def new_time(klass = Time)
485
+ klass.at(@value, @sub_second * 1_000_000)
486
+ end
487
+
488
+ # Constructs a new instance of a `DateTime` or `DateTime`-like class with
489
+ # the same {value}, {sub_second} and {utc_offset} as this {Timestamp}.
490
+ #
491
+ # @param klass [Class] the class to instantiate.
492
+ #
493
+ # @private
494
+ def new_datetime(klass = DateTime)
495
+ datetime = klass.jd(JD_EPOCH + ((@value.to_r + @sub_second) / 86400))
496
+ @utc_offset && @utc_offset != 0 ? datetime.new_offset(Rational(@utc_offset, 86400)) : datetime
497
+ end
498
+
499
+ private
500
+
501
+ # Converts the value and sub-seconds to a `String`, adding on the given
502
+ # offset.
503
+ #
504
+ # @param offset [Integer] the offset to add to the value.
505
+ # @return [String] the value and sub-seconds.
506
+ def value_and_sub_second_to_s(offset = 0)
507
+ "#{@value + offset}#{sub_second_to_s}"
508
+ end
509
+
510
+ # Converts the {sub_second} value to a `String` suitable for appending to
511
+ # the `String` representation of a {Timestamp}.
512
+ #
513
+ # @return [String] a `String` representation of {sub_second}.
514
+ def sub_second_to_s
515
+ if @sub_second == 0
516
+ ''
517
+ else
518
+ " #{@sub_second.numerator}/#{@sub_second.denominator}"
519
+ end
520
+ end
521
+
522
+ # Initializes a new {Timestamp} without validating the parameters. This
523
+ # method is used internally within {Timestamp} to avoid the overhead of
524
+ # checking parameters.
525
+ #
526
+ # @param value [Integer] the number of seconds since 1970-01-01 00:00:00 UTC
527
+ # ignoring leap seconds.
528
+ # @param sub_second [Numeric] the fractional part of the second as either a
529
+ # `Rational` that is greater than or equal to 0 and less than 1, or the
530
+ # `Integer` 0.
531
+ # @param utc_offset [Object] either `nil` for a {Timestamp} without a
532
+ # specified offset, an offset from UTC specified as an `Integer` number of
533
+ # seconds or the `Symbol` `:utc`).
534
+ def initialize!(value, sub_second = 0, utc_offset = nil)
535
+ @value = value
536
+
537
+ # Convert Rational(0,1) to 0.
538
+ @sub_second = sub_second == 0 ? 0 : sub_second
539
+
540
+ if utc_offset
541
+ @utc = utc_offset == :utc
542
+ @utc_offset = @utc ? 0 : utc_offset
543
+ else
544
+ @utc = @utc_offset = nil
545
+ end
546
+ end
547
+ end
548
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ # A subclass of {Timestamp} used to represent local times.
6
+ # {TimestampWithOffset} holds a reference to the related {TimezoneOffset} and
7
+ # overrides various methods to return results appropriate for the
8
+ # {TimezoneOffset}. Certain operations will clear the associated
9
+ # {TimezoneOffset} (if the {TimezoneOffset} would not necessarily be valid for
10
+ # the result). Once the {TimezoneOffset} has been cleared,
11
+ # {TimestampWithOffset} behaves identically to {Timestamp}.
12
+ class TimestampWithOffset < Timestamp
13
+ include WithOffset
14
+
15
+ # @return [TimezoneOffset] the {TimezoneOffset} associated with this
16
+ # instance.
17
+ attr_reader :timezone_offset
18
+
19
+ # Creates a new {TimestampWithOffset} from a given {Timestamp} and
20
+ # {TimezoneOffset}.
21
+ #
22
+ # @param timestamp [Timestamp] a {Timestamp}.
23
+ # @param timezone_offset [TimezoneOffset] a {TimezoneOffset} valid at the
24
+ # time of `timestamp`.
25
+ # @return [TimestampWithOffset] a {TimestampWithOffset} that has the same
26
+ # {value value} and {sub_second sub_second} as the `timestamp` parameter,
27
+ # a {utc_offset utc_offset} equal to the
28
+ # {TimezoneOffset#observed_utc_offset observed_utc_offset} of the
29
+ # `timezone_offset` parameter and {timezone_offset timezone_offset} set to
30
+ # the `timezone_offset` parameter.
31
+ # @raise [ArgumentError] if `timestamp` or `timezone_offset` is `nil`.
32
+ def self.set_timezone_offset(timestamp, timezone_offset)
33
+ raise ArgumentError, 'timestamp must be specified' unless timestamp
34
+ raise ArgumentError, 'timezone_offset must be specified' unless timezone_offset
35
+ new!(timestamp.value, timestamp.sub_second, timezone_offset.observed_utc_offset).set_timezone_offset(timezone_offset)
36
+ end
37
+
38
+ # Sets the associated {TimezoneOffset} of this {TimestampWithOffset}.
39
+ #
40
+ # @param timezone_offset [TimezoneOffset] a {TimezoneOffset} valid at the time
41
+ # and for the offset of this {TimestampWithOffset}.
42
+ # @return [TimestampWithOffset] `self`.
43
+ # @raise [ArgumentError] if `timezone_offset` is `nil`.
44
+ # @raise [ArgumentError] if {utc? self.utc?} is `true`.
45
+ # @raise [ArgumentError] if `timezone_offset.observed_utc_offset` does not equal
46
+ # `self.utc_offset`.
47
+ def set_timezone_offset(timezone_offset)
48
+ raise ArgumentError, 'timezone_offset must be specified' unless timezone_offset
49
+ raise ArgumentError, 'timezone_offset.observed_utc_offset does not match self.utc_offset' if utc? || utc_offset != timezone_offset.observed_utc_offset
50
+ @timezone_offset = timezone_offset
51
+ self
52
+ end
53
+
54
+ # An overridden version of {Timestamp#to_time} that, if there is an
55
+ # associated {TimezoneOffset}, returns a {TimeWithOffset} with that offset.
56
+ #
57
+ # @return [Time] if there is an associated {TimezoneOffset}, a
58
+ # {TimeWithOffset} representation of this {TimestampWithOffset}, otherwise
59
+ # a `Time` representation.
60
+ def to_time
61
+ to = timezone_offset
62
+ if to
63
+ new_time(TimeWithOffset).set_timezone_offset(to)
64
+ else
65
+ super
66
+ end
67
+ end
68
+
69
+ # An overridden version of {Timestamp#to_datetime}, if there is an
70
+ # associated {TimezoneOffset}, returns a {DateTimeWithOffset} with that
71
+ # offset.
72
+ #
73
+ # @return [DateTime] if there is an associated {TimezoneOffset}, a
74
+ # {DateTimeWithOffset} representation of this {TimestampWithOffset},
75
+ # otherwise a `DateTime` representation.
76
+ def to_datetime
77
+ to = timezone_offset
78
+ if to
79
+ new_datetime(DateTimeWithOffset).set_timezone_offset(to)
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end