class Timezone::Zone
This object represents a real-world timezone. Each instance provides methods for converting UTC times to the local timezone and local times to UTC for any historical, present or future times.
Constants
- DST_BIT
- NAME_BIT
- OFFSET_BIT
- RuleSet
- SOURCE_BIT
Attributes
@return [String] the timezone name
@return [String] the timezone name
Public Class Methods
Create a new timezone object using the timezone name.
@param name [String] the timezone name @return [Timezone::Zone]
# File lib/timezone/zone.rb, line 48 def initialize(name) @name = name end
Public Instance Methods
Compare one timezone with another based on current UTC offset.
@param other [Timezone::Zone] the other timezone
@return [-1, 0, 1, nil] comparison based on current `utc_offset`.
# File lib/timezone/zone.rb, line 174 def <=>(other) return nil unless other.respond_to?(:utc_offset) utc_offset <=> other.utc_offset end
The timezone abbreviation, at the given time.
@param time [#to_time] the source time @return [String] the timezone abbreviation, at the given time
# File lib/timezone/zone.rb, line 141 def abbr(time) time = sanitize(time) rule_for_utc(time)[NAME_BIT] end
If, at the given time, the timezone was observing Daylight Savings.
@param time [#to_time] the source time @return [Boolean] whether the timezone, at the given time, was
observing Daylight Savings Time
# File lib/timezone/zone.rb, line 152 def dst?(time) time = sanitize(time) rule_for_utc(time)[DST_BIT] end
@return [String] a developer friendly representation of the object
# File lib/timezone/zone.rb, line 24 def inspect "#<Timezone::Zone name: \"#{name}\">" end
Converts the given local time to the UTC equivalent.
@param time [#to_time] the local time @return [Time] the time in UTC
@note The UTC equivalent is a “best guess”. There are cases where
local times do not map to UTC at all (during a time skip forward). There are also cases where local times map to two distinct UTC times (during a fall back). All of these cases are approximated in this method and the first possible result is used instead.
@note A note about the handling of time arguments.
Because the UTC offset of a `Time` object in Ruby is not equivalent to a single timezone, the `time` argument in this method is first converted to a UTC equivalent before being used as a local time. This prevents confusion between historical UTC offsets and the UTC offset that the `Time` object provides. For instance, if I pass a "local" time with offset `+8` but the timezone actually had an offset of `+9` at the given historical time, there is an inconsistency that must be resolved. Did the user make a mistake; or is the offset intentional? One approach to solving this problem would be to raise an error, but this means that the user then needs to calculate the appropriate local offset and append that to a UTC time to satisfy the function. This is impractical because the offset can already be calculated by this library. The user should only need to provide a time without an offset! To resolve this inconsistency, the solution I chose was to scrub the offset. In the case where an offset is provided, the time is just converted to the UTC equivalent (without an offset). The resulting time is used as the local reference time. For example, if the time `08:00 +2` is passed to this function, the local time is assumed to be `06:00`.
# File lib/timezone/zone.rb, line 109 def local_to_utc(time) time = sanitize(time) (time - rule_for_local(time).rules.first[OFFSET_BIT]).utc end
Converts the given time to the local timezone and includes the UTC offset in the result.
@param time [#to_time] the source time @return [Time] the time in the local timezone with the UTC offset
# File lib/timezone/zone.rb, line 120 def time_with_offset(time) time = sanitize(time) utc = utc_to_local(time) offset = utc_offset(time) Time.new( utc.year, utc.month, utc.day, utc.hour, utc.min, utc.sec + utc.subsec, offset ) end
Return the UTC offset (in seconds) for the given time.
@param time [#to_time] (Time.now) the source time @return [Integer] the UTC offset (in seconds) in the local timezone
# File lib/timezone/zone.rb, line 162 def utc_offset(time = nil) time ||= Time.now time = sanitize(time) rule_for_utc(time)[OFFSET_BIT] end
Converts the given time to the local timezone and does not include a UTC offset in the result.
@param time [#to_time] the source time @return [Time] the time in the local timezone
@note The resulting time is always a UTC time. If you would like
a time with the appropriate offset, use `#time_with_offset` instead.
# File lib/timezone/zone.rb, line 61 def utc_to_local(time) time = sanitize(time) (time + utc_offset(time)).utc end
If this is a valid timezone.
@return [true] if this is a valid timezone
# File lib/timezone/zone.rb, line 31 def valid? true end
Private Instance Methods
Find the first rule that matches using binary search.
# File lib/timezone/zone.rb, line 254 def binary_search(rules, time, from = 0, to = nil, &block) to = rules.length - 1 if to.nil? return from if from == to mid = (from + to).div(2) unless yield(time, rules[mid]) return binary_search(rules, time, mid + 1, to, &block) end return mid if mid.zero? return mid unless yield(time, rules[mid - 1]) binary_search(rules, time, from, mid - 1, &block) end
# File lib/timezone/zone.rb, line 197 def rule_for_local(local) local = local.to_i rules = Loader.load(name) # For each rule, convert the local time into the UTC equivalent for # that rule offset, and then check if the UTC time matches the rule. index = binary_search(rules, local) do |t, r| match?(t - r[OFFSET_BIT], r) end match = rules[index] utc = local - match[OFFSET_BIT] # If the UTC rule for the calculated UTC time does not map back to the # same rule, then we have a skip in time and there is no applicable rule. return RuleSet.new(:missing, [match]) if rule_for_utc(utc) != match # If the match is the last rule, then return it. return RuleSet.new(:single, [match]) if index == rules.length - 1 # If the UTC equivalent time falls within the last hour(s) of the time # change which were replayed during a fall-back in time, then return # the matched rule and the next one. # # Example: # # rules = [ # [ 8:00 UTC, -1 ], # UTC-1 up to and including 8:00 UTC # [ 14:00 UTC, -2 ], # UTC-2 up to and including 14:00 UTC # ] # # 6:50 local (7:50 UTC) by the first rule # 6:50 local (8:50 UTC) by the second rule # # Since both rules provide valid mappings for the local time, # we need to return both values. last_hour = match[SOURCE_BIT] - match[OFFSET_BIT] + rules[index + 1][OFFSET_BIT] if utc > last_hour RuleSet.new(:double, rules[index..(index + 1)]) else RuleSet.new(:single, [match]) end end
# File lib/timezone/zone.rb, line 246 def rule_for_utc(time) time = time.to_i rules = Loader.load(name) rules[binary_search(rules, time) { |t, r| match?(t, r) }] end
# File lib/timezone/zone.rb, line 182 def sanitize(time) time.to_time end