123456789_123456789_123456789_123456789_123456789_

Class: Timezone::Zone

Relationships & Source Files
Namespace Children
Classes:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
self, Comparable
Inherits: Object
Defined in: lib/timezone/zone.rb

Overview

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.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(name) ⇒ Zone

Create a new timezone object using the timezone name.

Parameters:

  • name (String)

    the timezone name

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 47

def initialize(name)
  @name = name
end

Instance Attribute Details

#nameString (readonly) Also known as: #to_s

Returns:

  • (String)

    the timezone name

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 18

attr_reader :name

#to_s (readonly)

Alias for #name.

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 20

alias to_s name

#valid?true (readonly)

If this is a valid timezone.

Returns:

  • (true)

    if this is a valid timezone

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 30

def valid?
  true
end

Instance Method Details

#<=>(other) ⇒ -1, ...

Compare one timezone with another based on current UTC offset.

Parameters:

  • other (Zone)

    the other timezone

Returns:

  • (-1, 0, 1, nil)

    comparison based on current #utc_offset.

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 173

def <=>(other)
  return nil unless other.respond_to?(:utc_offset)

  utc_offset <=> other.utc_offset
end

#abbr(time) ⇒ String

The timezone abbreviation, at the given time.

Parameters:

  • time (#to_time)

    the source time

Returns:

  • (String)

    the timezone abbreviation, at the given time

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 140

def abbr(time)
  time = sanitize(time)

  rule_for_utc(time)[NAME_BIT]
end

#binary_search(rules, time, from = 0, to = nil, &block) (private)

Find the first rule that matches using binary search.

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 253

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

#dst?(time) ⇒ Boolean

If, at the given time, the timezone was observing Daylight Savings.

Parameters:

  • time (#to_time)

    the source time

Returns:

  • (Boolean)

    whether the timezone, at the given time, was observing Daylight Savings Time

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 151

def dst?(time)
  time = sanitize(time)

  rule_for_utc(time)[DST_BIT]
end

#inspectString

Returns:

  • (String)

    a developer friendly representation of the object

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 23

def inspect
  "#<Timezone::Zone name: \"#{name}\">"
end

#local_to_utc(time) ⇒ Time

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.

Converts the given local time to the UTC equivalent.

Parameters:

  • time (#to_time)

    the local time

Returns:

  • (Time)

    the time in UTC

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 108

def local_to_utc(time)
  time = sanitize(time)

  (time - rule_for_local(time).rules.first[OFFSET_BIT]).utc
end

#match?(seconds, rule) ⇒ Boolean (private)

This method is for internal use only.

Does the given time (in seconds) match this rule?

Each rule has a SOURCE bit which is the number of seconds, since the Epoch, up to which the rule is valid.

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 189

def match?(seconds, rule) #:nodoc:
  seconds <= rule[SOURCE_BIT]
end

#rule_for_local(local) (private)

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 196

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) (private)

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 245

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) (private)

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 181

def sanitize(time)
  time.to_time
end

#time(time)

Alias for #utc_to_local.

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 66

alias time utc_to_local

#time_with_offset(time) ⇒ Time

Converts the given time to the local timezone and includes the UTC offset in the result.

Parameters:

  • time (#to_time)

    the source time

Returns:

  • (Time)

    the time in the local timezone with the UTC offset

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 119

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) ⇒ Integer

Return the UTC offset (in seconds) for the given time.

Parameters:

  • time (#to_time) (defaults to: nil)

    (Time.now) the source time

Returns:

  • (Integer)

    the UTC offset (in seconds) in the local timezone

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 161

def utc_offset(time = nil)
  time ||= Time.now
  time = sanitize(time)

  rule_for_utc(time)[OFFSET_BIT]
end

#utc_to_local(time) ⇒ Time Also known as: #time

Note:

The resulting time is always a UTC time. If you would like a time with the appropriate offset, use #time_with_offset instead.

Converts the given time to the local timezone and does not include a UTC offset in the result.

Parameters:

  • time (#to_time)

    the source time

Returns:

  • (Time)

    the time in the local timezone

[ GitHub ]

  
# File 'lib/timezone/zone.rb', line 60

def utc_to_local(time)
  time = sanitize(time)

  (time + utc_offset(time)).utc
end