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

name[R]

@return [String] the timezone name

to_s[R]

@return [String] the timezone name

Public Class Methods

new(name) click to toggle source

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

<=>(other) click to toggle source

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
abbr(time) click to toggle source

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
dst?(time) click to toggle source

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
inspect() click to toggle source

@return [String] a developer friendly representation of the object

# File lib/timezone/zone.rb, line 24
def inspect
  "#<Timezone::Zone name: \"#{name}\">"
end
local_to_utc(time) click to toggle source

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
time(time)
Alias for: utc_to_local
time_with_offset(time) click to toggle source

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
utc_offset(time = nil) click to toggle source

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
utc_to_local(time) click to toggle source

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
Also aliased as: time
valid?() click to toggle source

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

rule_for_local(local) click to toggle source
# 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
rule_for_utc(time) click to toggle source
# 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
sanitize(time) click to toggle source
# File lib/timezone/zone.rb, line 182
def sanitize(time)
  time.to_time
end