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,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