tzinfo 1.2.10 → 2.0.6

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