tzinfo 1.2.6 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +1 -1
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +482 -380
  6. data/LICENSE +12 -12
  7. data/README.md +368 -114
  8. data/lib/tzinfo.rb +64 -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 +143 -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 +575 -0
  21. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +286 -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 +43 -99
  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 -176
  63. data/lib/tzinfo/ruby_country_info.rb +0 -74
  64. data/lib/tzinfo/ruby_data_source.rb +0 -138
  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 -298
  74. data/test/tc_country.rb +0 -236
  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 -165
  88. data/test/tc_time_or_datetime.rb +0 -660
  89. data/test/tc_timezone.rb +0 -1359
  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 -1234
  106. data/test/test_utils.rb +0 -188
  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