tzinfo 1.2.7 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +489 -382
  6. data/LICENSE +12 -12
  7. data/README.md +368 -114
  8. data/lib/tzinfo.rb +59 -29
  9. data/lib/tzinfo/country.rb +141 -129
  10. data/lib/tzinfo/country_timezone.rb +70 -112
  11. data/lib/tzinfo/data_source.rb +389 -144
  12. data/lib/tzinfo/data_sources.rb +8 -0
  13. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  14. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  15. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  16. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  17. data/lib/tzinfo/data_sources/ruby_data_source.rb +145 -0
  18. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  19. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  20. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +577 -0
  21. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +288 -0
  22. data/lib/tzinfo/data_timezone.rb +33 -47
  23. data/lib/tzinfo/datetime_with_offset.rb +153 -0
  24. data/lib/tzinfo/format1.rb +10 -0
  25. data/lib/tzinfo/format1/country_definer.rb +17 -0
  26. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  27. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  28. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  29. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  30. data/lib/tzinfo/format2.rb +10 -0
  31. data/lib/tzinfo/format2/country_definer.rb +68 -0
  32. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  33. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  34. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  35. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  36. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  37. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
  38. data/lib/tzinfo/info_timezone.rb +26 -21
  39. data/lib/tzinfo/linked_timezone.rb +33 -52
  40. data/lib/tzinfo/offset_timezone_period.rb +42 -0
  41. data/lib/tzinfo/string_deduper.rb +118 -0
  42. data/lib/tzinfo/time_with_offset.rb +128 -0
  43. data/lib/tzinfo/timestamp.rb +548 -0
  44. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  45. data/lib/tzinfo/timezone.rb +989 -502
  46. data/lib/tzinfo/timezone_offset.rb +84 -74
  47. data/lib/tzinfo/timezone_period.rb +151 -217
  48. data/lib/tzinfo/timezone_proxy.rb +70 -79
  49. data/lib/tzinfo/timezone_transition.rb +77 -109
  50. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  51. data/lib/tzinfo/untaint_ext.rb +18 -0
  52. data/lib/tzinfo/version.rb +7 -0
  53. data/lib/tzinfo/with_offset.rb +61 -0
  54. metadata +42 -98
  55. metadata.gz.sig +0 -0
  56. data/Rakefile +0 -107
  57. data/lib/tzinfo/country_index_definition.rb +0 -31
  58. data/lib/tzinfo/country_info.rb +0 -42
  59. data/lib/tzinfo/data_timezone_info.rb +0 -55
  60. data/lib/tzinfo/linked_timezone_info.rb +0 -26
  61. data/lib/tzinfo/offset_rationals.rb +0 -77
  62. data/lib/tzinfo/ruby_core_support.rb +0 -169
  63. data/lib/tzinfo/ruby_country_info.rb +0 -74
  64. data/lib/tzinfo/ruby_data_source.rb +0 -140
  65. data/lib/tzinfo/time_or_datetime.rb +0 -340
  66. data/lib/tzinfo/timezone_definition.rb +0 -36
  67. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  68. data/lib/tzinfo/timezone_info.rb +0 -30
  69. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  70. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  71. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  72. data/lib/tzinfo/zoneinfo_data_source.rb +0 -496
  73. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -300
  74. data/test/tc_country.rb +0 -238
  75. data/test/tc_country_index_definition.rb +0 -69
  76. data/test/tc_country_info.rb +0 -16
  77. data/test/tc_country_timezone.rb +0 -173
  78. data/test/tc_data_source.rb +0 -218
  79. data/test/tc_data_timezone.rb +0 -99
  80. data/test/tc_data_timezone_info.rb +0 -18
  81. data/test/tc_info_timezone.rb +0 -34
  82. data/test/tc_linked_timezone.rb +0 -155
  83. data/test/tc_linked_timezone_info.rb +0 -23
  84. data/test/tc_offset_rationals.rb +0 -23
  85. data/test/tc_ruby_core_support.rb +0 -168
  86. data/test/tc_ruby_country_info.rb +0 -110
  87. data/test/tc_ruby_data_source.rb +0 -167
  88. data/test/tc_time_or_datetime.rb +0 -660
  89. data/test/tc_timezone.rb +0 -1361
  90. data/test/tc_timezone_definition.rb +0 -113
  91. data/test/tc_timezone_index_definition.rb +0 -73
  92. data/test/tc_timezone_info.rb +0 -11
  93. data/test/tc_timezone_london.rb +0 -143
  94. data/test/tc_timezone_melbourne.rb +0 -142
  95. data/test/tc_timezone_new_york.rb +0 -142
  96. data/test/tc_timezone_offset.rb +0 -126
  97. data/test/tc_timezone_period.rb +0 -555
  98. data/test/tc_timezone_proxy.rb +0 -136
  99. data/test/tc_timezone_transition.rb +0 -366
  100. data/test/tc_timezone_transition_definition.rb +0 -295
  101. data/test/tc_timezone_utc.rb +0 -27
  102. data/test/tc_transition_data_timezone_info.rb +0 -433
  103. data/test/tc_zoneinfo_country_info.rb +0 -78
  104. data/test/tc_zoneinfo_data_source.rb +0 -1204
  105. data/test/tc_zoneinfo_timezone_info.rb +0 -1236
  106. data/test/test_utils.rb +0 -192
  107. data/test/ts_all.rb +0 -7
  108. data/test/ts_all_ruby.rb +0 -5
  109. data/test/ts_all_zoneinfo.rb +0 -9
  110. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  111. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  112. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -315
  113. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -218
  114. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  115. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  116. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  117. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  118. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -261
  119. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -186
  120. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -321
  121. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -265
  122. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -220
  123. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  124. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -927
  125. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -596
  126. data/test/tzinfo-data/tzinfo/data/version.rb +0 -14
  127. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  128. data/test/zoneinfo/America/New_York +0 -0
  129. data/test/zoneinfo/Australia/Melbourne +0 -0
  130. data/test/zoneinfo/EST +0 -0
  131. data/test/zoneinfo/Etc/UTC +0 -0
  132. data/test/zoneinfo/Europe/Amsterdam +0 -0
  133. data/test/zoneinfo/Europe/Andorra +0 -0
  134. data/test/zoneinfo/Europe/London +0 -0
  135. data/test/zoneinfo/Europe/Paris +0 -0
  136. data/test/zoneinfo/Europe/Prague +0 -0
  137. data/test/zoneinfo/Factory +0 -0
  138. data/test/zoneinfo/iso3166.tab +0 -275
  139. data/test/zoneinfo/leapseconds +0 -61
  140. data/test/zoneinfo/posix/Europe/London +0 -0
  141. data/test/zoneinfo/posixrules +0 -0
  142. data/test/zoneinfo/right/Europe/London +0 -0
  143. data/test/zoneinfo/zone.tab +0 -439
  144. data/test/zoneinfo/zone1970.tab +0 -369
  145. data/tzinfo.gemspec +0 -21
@@ -0,0 +1,118 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'concurrent'
5
+
6
+ module TZInfo
7
+ # Maintains a pool of `String` instances. The {#dedupe} method will return
8
+ # either a pooled copy of a given `String` or add the instance to the pool.
9
+ #
10
+ # @private
11
+ class StringDeduper #:nodoc:
12
+ class << self
13
+ # @return [StringDeduper] a globally available singleton instance of
14
+ # {StringDeduper}. This instance is safe for use in concurrently
15
+ # executing threads.
16
+ attr_reader :global
17
+ end
18
+
19
+ # Initializes a new {StringDeduper}.
20
+ def initialize
21
+ @strings = create_hash do |h, k|
22
+ v = k.dup.freeze
23
+ h[v] = v
24
+ end
25
+ end
26
+
27
+ # @param string [String] the string to deduplicate.
28
+ # @return [bool] `string` if it is frozen, otherwise a frozen, possibly
29
+ # pre-existing copy of `string`.
30
+ def dedupe(string)
31
+ return string if string.frozen?
32
+ @strings[string]
33
+ end
34
+
35
+ protected
36
+
37
+ # Creates a `Hash` to store pooled `String` instances.
38
+ #
39
+ # @param block [Proc] Default value block to be passed to `Hash.new`.
40
+ # @return [Hash] a `Hash` to store pooled `String` instances.
41
+ def create_hash(&block)
42
+ Hash.new(&block)
43
+ end
44
+ end
45
+ private_constant :StringDeduper
46
+
47
+ # A thread-safe version of {StringDeduper}.
48
+ #
49
+ # @private
50
+ class ConcurrentStringDeduper < StringDeduper #:nodoc:
51
+ protected
52
+
53
+ def create_hash(&block)
54
+ Concurrent::Map.new(&block)
55
+ end
56
+ end
57
+ private_constant :ConcurrentStringDeduper
58
+
59
+
60
+ string_unary_minus_does_dedupe = if '0'.respond_to?(:-@)
61
+ # :nocov_no_string_-@:
62
+ s1 = -('0'.dup)
63
+ s2 = -('0'.dup)
64
+ s1.object_id == s2.object_id
65
+ # :nocov_no_string_-@:
66
+ else
67
+ # :nocov_string_-@:
68
+ false
69
+ # :nocov_string_-@:
70
+ end
71
+
72
+ if string_unary_minus_does_dedupe
73
+ # :nocov_no_deduping_string_unary_minus:
74
+
75
+ # An implementation of {StringDeduper} using the `String#-@` method where
76
+ # that method performs deduplication (Ruby 2.5 and later).
77
+ #
78
+ # Note that this is slightly different to the plain {StringDeduper}
79
+ # implementation. In this implementation, frozen literal strings are already
80
+ # in the pool and are candidates for being returned, even when passed
81
+ # another equal frozen non-literal string. {StringDeduper} will always
82
+ # return frozen strings.
83
+ #
84
+ # There are also differences in encoding handling. This implementation will
85
+ # treat strings with different encodings as different strings.
86
+ # {StringDeduper} will treat strings with the compatible encodings as the
87
+ # same string.
88
+ #
89
+ # @private
90
+ class UnaryMinusGlobalStringDeduper #:nodoc:
91
+ # @param string [String] the string to deduplicate.
92
+ # @return [bool] `string` if it is frozen, otherwise a frozen, possibly
93
+ # pre-existing copy of `string`.
94
+ def dedupe(string)
95
+ # String#-@ on Ruby 2.6 will dedupe a frozen non-literal String. Ruby
96
+ # 2.5 will just return frozen strings.
97
+ #
98
+ # The pooled implementation can't tell the difference between frozen
99
+ # literals and frozen non-literals, so must always return frozen String
100
+ # instances to avoid doing unncessary work when loading format 2
101
+ # TZInfo::Data modules.
102
+ #
103
+ # For compatibility with the pooled implementation, just return frozen
104
+ # string instances (acting like Ruby 2.5).
105
+ return string if string.frozen?
106
+ -string
107
+ end
108
+ end
109
+ private_constant :UnaryMinusGlobalStringDeduper
110
+
111
+ StringDeduper.instance_variable_set(:@global, UnaryMinusGlobalStringDeduper.new)
112
+ # :nocov_no_deduping_string_unary_minus:
113
+ else
114
+ # :nocov_deduping_string_unary_minus:
115
+ StringDeduper.instance_variable_set(:@global, ConcurrentStringDeduper.new)
116
+ # :nocov_deduping_string_unary_minus:
117
+ end
118
+ end
@@ -0,0 +1,128 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ # A subclass of `Time` used to represent local times. {TimeWithOffset} holds a
6
+ # reference to the related {TimezoneOffset} and overrides various methods to
7
+ # return results appropriate for the {TimezoneOffset}. Certain operations will
8
+ # clear the associated {TimezoneOffset} (if the {TimezoneOffset} would not
9
+ # necessarily be valid for the result). Once the {TimezoneOffset} has been
10
+ # cleared, {TimeWithOffset} behaves identically to `Time`.
11
+ #
12
+ # Arithmetic performed on {TimeWithOffset} instances is _not_ time zone-aware.
13
+ # Regardless of whether transitions in the time zone are crossed, results of
14
+ # arithmetic operations will always maintain the same offset from UTC
15
+ # (`utc_offset`). The associated {TimezoneOffset} will aways be cleared.
16
+ class TimeWithOffset < Time
17
+ include WithOffset
18
+
19
+ # @return [TimezoneOffset] the {TimezoneOffset} associated with this
20
+ # instance.
21
+ attr_reader :timezone_offset
22
+
23
+ # Marks this {TimeWithOffset} as a local time with the UTC offset of a given
24
+ # {TimezoneOffset} and sets the associated {TimezoneOffset}.
25
+ #
26
+ # @param timezone_offset [TimezoneOffset] the {TimezoneOffset} to use to set
27
+ # the offset of this {TimeWithOffset}.
28
+ # @return [TimeWithOffset] `self`.
29
+ # @raise [ArgumentError] if `timezone_offset` is `nil`.
30
+ def set_timezone_offset(timezone_offset)
31
+ raise ArgumentError, 'timezone_offset must be specified' unless timezone_offset
32
+ localtime(timezone_offset.observed_utc_offset)
33
+ @timezone_offset = timezone_offset
34
+ self
35
+ end
36
+
37
+ # An overridden version of `Time#dst?` that, if there is an associated
38
+ # {TimezoneOffset}, returns the result of calling {TimezoneOffset#dst? dst?}
39
+ # on that offset.
40
+ #
41
+ # @return [Boolean] `true` if daylight savings time is being observed,
42
+ # otherwise `false`.
43
+ def dst?
44
+ to = timezone_offset
45
+ to ? to.dst? : super
46
+ end
47
+ alias isdst dst?
48
+
49
+ # An overridden version of `Time#gmtime` that clears the associated
50
+ # {TimezoneOffset}.
51
+ #
52
+ # @return [TimeWithOffset] `self`.
53
+ def gmtime
54
+ super
55
+ @timezone_offset = nil
56
+ self
57
+ end
58
+
59
+ # An overridden version of `Time#localtime` that clears the associated
60
+ # {TimezoneOffset}.
61
+ #
62
+ # @return [TimeWithOffset] `self`.
63
+ def localtime(*args)
64
+ super
65
+ @timezone_offset = nil
66
+ self
67
+ end
68
+
69
+ # An overridden version of `Time#round` that, if there is an associated
70
+ # {TimezoneOffset}, returns a {TimeWithOffset} preserving that offset.
71
+ #
72
+ # @return [Time] the rounded time.
73
+ def round(ndigits = 0)
74
+ if_timezone_offset(super) {|o,t| self.class.at(t.to_i, t.subsec * 1_000_000).set_timezone_offset(o) }
75
+ end
76
+
77
+ # An overridden version of `Time#to_a`. The `isdst` (index 8) and `zone`
78
+ # (index 9) elements of the array are set according to the associated
79
+ # {TimezoneOffset}.
80
+ #
81
+ # @return [Array] an `Array` representation of the {TimeWithOffset}.
82
+ def to_a
83
+ if_timezone_offset(super) do |o,a|
84
+ a[8] = o.dst?
85
+ a[9] = o.abbreviation
86
+ a
87
+ end
88
+ end
89
+
90
+ # An overridden version of `Time#utc` that clears the associated
91
+ # {TimezoneOffset}.
92
+ #
93
+ # @return [TimeWithOffset] `self`.
94
+ def utc
95
+ super
96
+ @timezone_offset = nil
97
+ self
98
+ end
99
+
100
+ # An overridden version of `Time#zone` that, if there is an associated
101
+ # {TimezoneOffset}, returns the {TimezoneOffset#abbreviation abbreviation}
102
+ # of that offset.
103
+ #
104
+ # @return [String] the {TimezoneOffset#abbreviation abbreviation} of the
105
+ # associated {TimezoneOffset}, or the result from `Time#zone` if there is
106
+ # no such offset.
107
+ def zone
108
+ to = timezone_offset
109
+ to ? to.abbreviation : super
110
+ end
111
+
112
+ # An overridden version of `Time#to_datetime` that, if there is an
113
+ # associated {TimezoneOffset}, returns a {DateTimeWithOffset} with that
114
+ # offset.
115
+ #
116
+ # @return [DateTime] if there is an associated {TimezoneOffset}, a
117
+ # {DateTimeWithOffset} representation of this {TimeWithOffset}, otherwise
118
+ # a `Time` representation.
119
+ def to_datetime
120
+ if_timezone_offset(super) do |o,dt|
121
+ offset = dt.offset
122
+ result = DateTimeWithOffset.jd(dt.jd + dt.day_fraction - offset)
123
+ result = result.new_offset(offset) unless offset == 0
124
+ result.set_timezone_offset(o)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -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