module EtOrbi

Constants

VERSION
ZONES_ISO8601_REX
ZONES_OLSON
ZONE_ALIASES

docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones support.microsoft.com/en-ca/help/973627/microsoft-time-zone-index-values ss64.com/nt/timezones.html

Attributes

_os_zone[RW]

Public Class Methods

_make_info() click to toggle source

For `make info`

# File lib/et-orbi/info.rb, line 36
def _make_info

  puts render_nozone_time(Time.now.to_f)
  puts platform_info
end
centos_tz() click to toggle source
# File lib/et-orbi/zones.rb, line 109
def centos_tz

  path = '/etc/sysconfig/clock'

  File.open(path, 'rb') do |f|
    until f.eof?
      if m = f.readline.match(/ZONE="([^"]+)"/); return m[1]; end
    end
  end if File.exist?(path)

  nil
rescue; nil; end
chronic_enabled=(b) click to toggle source
# File lib/et-orbi/make.rb, line 9
def self.chronic_enabled=(b)
  @chronic_enabled = b
end
chronic_enabled?() click to toggle source
# File lib/et-orbi/make.rb, line 6
def self.chronic_enabled?
  @chronic_enabled
end
debian_tz() click to toggle source

system tz determination

# File lib/et-orbi/zones.rb, line 102
def debian_tz

  path = '/etc/timezone'

  File.exist?(path) ? File.read(path).strip : nil
rescue; nil; end
determine_local_tzone() click to toggle source
# File lib/et-orbi/zones.rb, line 53
def determine_local_tzone

  # ENV has the priority

  etz = ENV['TZ']

  tz = etz && get_tzone(etz)
  return tz if tz

  # then Rails/ActiveSupport has the priority

  if Time.respond_to?(:zone) && Time.zone.respond_to?(:tzinfo)
    tz = Time.zone.tzinfo
    return tz if tz
  end

  # then the operating system is queried

  tz = ::TZInfo::Timezone.get(os_tz) rescue nil
  return tz if tz

  # then Ruby's time zone abbs are looked at CST, JST, CEST, ... :-(

  tzs = determine_local_tzones
  tz = (etz && tzs.find { |z| z.name == etz }) || tzs.first
  return tz if tz

  # then, fall back to GMT offest :-(

  n = Time.now

  get_tzone(n.zone) ||
  get_tzone(n.strftime('%Z%z'))
end
Also aliased as: zone
extract_zone(str) click to toggle source
# File lib/et-orbi/zones.rb, line 35
def extract_zone(str)

  s = str.dup

  zs = ZONES_OLSON
    .inject([]) { |a, z|
      i = s.index(z); next a unless i
      a << z
      s[i, z.length] = ''
      a }

  s.gsub!(ZONES_ISO8601_REX) { |m| zs << m.strip; '' } #if zs.empty?

  zs = zs.sort_by { |z| str.index(z) }

  [ s.strip, zs.last ]
end
gather_tzs() click to toggle source
# File lib/et-orbi/zones.rb, line 131
def gather_tzs

  { :debian => debian_tz, :centos => centos_tz, :osx => osx_tz }
end
get_tzone(o) click to toggle source
# File lib/et-orbi/zone.rb, line 6
def get_tzone(o)

  return o if o.is_a?(::TZInfo::Timezone)
  return nil if o == nil
  return determine_local_tzone if o == :local
  return ::TZInfo::Timezone.get('Zulu') if o == 'Z'
  return o.tzinfo if o.respond_to?(:tzinfo)

  o = to_offset(o) if o.is_a?(Numeric)

  return nil unless o.is_a?(String)

  s = tweak_zone_name(o)

  get_offset_tzone(s) ||
  get_x_offset_tzone(s) ||
  get_tzinfo_tzone(s)
end
list_iso8601_zones(s) click to toggle source

en.wikipedia.org/wiki/ISO_8601 Postel's law applies

# File lib/et-orbi/zones.rb, line 21
def list_iso8601_zones(s)

  s.scan(ZONES_ISO8601_REX).collect(&:strip)
end
make(*a)
Alias for: make_time
make_time(*a) click to toggle source
# File lib/et-orbi/make.rb, line 51
def make_time(*a)

  zone = a.length > 1 ? get_tzone(a.last) : nil
  a.pop if zone

  o = a.length > 1 ? a : a.first

  case o
  when Time then make_from_time(o, zone)
  when Date then make_from_date(o, zone)
  when Array then make_from_array(o, zone)
  when String then make_from_string(o, zone)
  when Numeric then make_from_numeric(o, zone)
  when ::EtOrbi::EoTime then make_from_eotime(o, zone)
  else fail ArgumentError.new(
    "Cannot turn #{o.inspect} to a ::EtOrbi::EoTime instance")
  end
end
Also aliased as: make
now(zone=nil) click to toggle source
# File lib/et-orbi/make.rb, line 15
def now(zone=nil)

  EoTime.new(Time.now.to_f, zone)
end
os_tz() click to toggle source
# File lib/et-orbi/zones.rb, line 91
def os_tz

  return (@_os_zone == '' ? nil : @_os_zone) \
    if defined?(@_os_zone) && @_os_zone

  @os_tz ||= (debian_tz || centos_tz || osx_tz)
end
osx_tz() click to toggle source
# File lib/et-orbi/zones.rb, line 122
def osx_tz

  path = '/etc/localtime'

  File.symlink?(path) ?
    File.readlink(path).split('/')[4..-1].join('/') :
    nil
rescue; nil; end
parse(str, opts={}) click to toggle source
# File lib/et-orbi/make.rb, line 20
def parse(str, opts={})

  str, str_zone = extract_zone(str)

  if t = chronic_parse(str, opts)
    str = [ t.strftime('%F %T'), str_zone ].compact.join(' ')
  end

  dt =
    begin
      DateTime.parse(str)
    rescue
      fail ArgumentError, "No time information in #{str.inspect}"
    end
    #end if RUBY_VERSION < '1.9.0'
    #end if RUBY_VERSION < '2.0.0'
      #
      # is necessary since Time.parse('xxx') in Ruby < 1.9 yields `now`

  zone =
    opts[:zone] ||
    get_tzone(str_zone) ||
    determine_local_tzone

  #local = Time.parse(str)
  #secs = zone.local_to_utc(local).to_f
  secs = zone.local_to_utc(dt).to_time.to_f

  EoTime.new(secs, zone)
end
platform_info() click to toggle source
# File lib/et-orbi/info.rb, line 6
def platform_info

  etos = Proc.new { |k, v| "#{k}:#{v.inspect}" }

  h = {
    'etz' => ENV['TZ'],
    'tnz' => Time.now.zone,
    'tziv' => tzinfo_version,
    'tzidv' => tzinfo_data_version,
    'rv' => RUBY_VERSION,
    'rp' => RUBY_PLATFORM,
    'win' => Gem.win_platform?,
    'rorv' => (Rails::VERSION::STRING rescue nil),
    'astz' => ([ Time.zone.class, Time.zone.tzinfo.name ] rescue nil),
    'eov' => EtOrbi::VERSION,
    'eotnz' => '???',
    'eotnfz' => '???',
    'eotlzn' => '???' }
  if ltz = EtOrbi::EoTime.local_tzone
    h['eotnz'] = EtOrbi::EoTime.now.zone
    h['eotnfz'] = EtOrbi::EoTime.now.strftime('%z')
    h['eotnfZ'] = EtOrbi::EoTime.now.strftime('%Z')
    h['eotlzn'] = ltz.name
  end

  "(#{h.map(&etos).join(',')},#{gather_tzs.map(&etos).join(',')})"
end
render_nozone_time(seconds) click to toggle source
# File lib/et-orbi/info.rb, line 42
def render_nozone_time(seconds)

  t =
    Time.utc(1970) + seconds
  ts =
    t.strftime('%Y-%m-%d %H:%M:%S') +
    ".#{(seconds % 1).to_s.split('.').last}"
  tz =
    EtOrbi.determine_local_tzone
  z =
    tz ? tz.period_for_local(t).abbreviation.to_s : nil

  "(secs:#{seconds},utc~:#{ts.inspect},ltz~:#{z.inspect})"
end
tweak_zone_name(name) click to toggle source
# File lib/et-orbi/zones.rb, line 164
def tweak_zone_name(name)

  return name unless (name.match(/./) rescue nil)
    # to prevent invalid byte sequence in UTF-8..., gh-15

  normalize(name) ||
  unzz(name) ||
  name
end
windows_zone_name(zone_name, time) click to toggle source

Semi-helpful, since it requires the current time

# File lib/et-orbi/zones.rb, line 138
def windows_zone_name(zone_name, time)

  twin = Time.utc(time.year, 1, 1) # winter
  tsum = Time.utc(time.year, 7, 1) # summer

  tz = ::TZInfo::Timezone.get(zone_name)
  tzo = tz.period_for_local(time).utc_total_offset
  tzop = tzo < 0 ? nil : '-'; tzo = tzo.abs
  tzoh = tzo / 3600
  tzos = tzo % 3600
  tzos = tzos == 0 ? nil : ':%02d' % (tzos / 60)

  abbs = [
    tz.period_for_utc(twin).abbreviation.to_s,
    tz.period_for_utc(tsum).abbreviation.to_s ]
      .uniq

  if abbs[0].match(/\A[A-Z]/)
    [ abbs[0], tzop, tzoh, tzos, abbs[1] ]
      .compact.join
  else
    [ windows_zone_code_x(zone_name), tzop, tzoh, tzos || ':00', zone_name ]
      .collect(&:to_s).join
  end
end
zone()

Protected Class Methods

chronic_parse(str, opts) click to toggle source
# File lib/et-orbi/make.rb, line 73
def chronic_parse(str, opts)

  return false unless defined?(::Chronic)
  return false unless opts.fetch(:enable_chronic) { self.chronic_enabled? }

  os = opts
    .select { |k, _| Chronic::Parser::DEFAULT_OPTIONS.keys.include?(k) }

  ::Chronic.parse(str, os)
end
create_offset_tzone(utc_off, id) click to toggle source

TZInfo >= 2.0.0

# File lib/et-orbi/zone.rb, line 56
def create_offset_tzone(utc_off, id)

  off = TZInfo::TimezoneOffset.new(utc_off, 0, id)
  tzi = TZInfo::DataSources::ConstantOffsetDataTimezoneInfo.new(id, off)
  tzi.create_timezone
end
custom_tzs() click to toggle source
# File lib/et-orbi/zones.rb, line 240
def custom_tzs; @custom_tzs ||= {}; end
determine_local_tzones() click to toggle source
# File lib/et-orbi/zones.rb, line 210
def determine_local_tzones

  tabbs = (-6..5)
    .collect { |i|
      t = Time.now + i * 30 * 24 * 3600
      "#{t.zone}_#{t.utc_offset}" }
    .uniq
    .sort
    .join('|')

  t = Time.now
  #tu = t.dup.utc # /!\ dup is necessary, #utc modifies its target

  twin = Time.local(t.year, 1, 1) # winter
  tsum = Time.local(t.year, 7, 1) # summer

  @tz_winter_summer ||= {}

  @tz_winter_summer[tabbs] ||= tz_all
    .select { |tz|
      pw = tz.period_for_local(twin)
      ps = tz.period_for_local(tsum)
      tabbs ==
        [ "#{pw.abbreviation}_#{pw.utc_total_offset}",
          "#{ps.abbreviation}_#{ps.utc_total_offset}" ]
          .uniq.sort.join('|') }

  @tz_winter_summer[tabbs]
end
get_as_tzone(t) click to toggle source

api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html

If it responds to time_zone, then return that time zone.

# File lib/et-orbi/zone.rb, line 126
def get_as_tzone(t)

  t.respond_to?(:time_zone) ? t.time_zone : nil
end
get_local_tzone(t) click to toggle source
# File lib/et-orbi/zone.rb, line 115
def get_local_tzone(t)

  l = Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec, t.usec)

  (t.zone == l.zone) ? determine_local_tzone : nil
end
get_offset_tzone(str) click to toggle source

custom timezones, no DST, just an offset, like “+08:00” or “-01:30”

# File lib/et-orbi/zone.rb, line 29
def get_offset_tzone(str)

  m = str.match(/\A([+-][0-1]?[0-9]):?([0-5][0-9])?\z/) rescue nil
    #
    # On Windows, the real encoding could be something other than UTF-8,
    # and make the match fail
    #
  return nil unless m

  tz = custom_tzs[str]
  return tz if tz

  hr = m[1].to_i
  mn = m[2].to_i

  hr = nil if hr.abs > 11
  hr = nil if mn > 59
  mn = -mn if hr && hr < 0

  hr ?
    custom_tzs[str] = create_offset_tzone(hr * 3600 + mn * 60, str) :
    nil
end
get_tzinfo_tzone(name) click to toggle source
# File lib/et-orbi/zone.rb, line 95
def get_tzinfo_tzone(name)

  #return ::TZInfo::Timezone.get(name) rescue nil

  loop do
    return ::TZInfo::Timezone.get(name) if ZONES_OLSON.include?(name)
    name = name[0..-2]
    return nil if name.empty?
  end
end
get_x_offset_tzone(str) click to toggle source
# File lib/et-orbi/zone.rb, line 74
def get_x_offset_tzone(str)

  m = str.match(/\A_..-?[0-1]?\d:?(?:[0-5]\d)?(.+)\z/) rescue nil
    #
    # On Windows, the real encoding could be something other than UTF-8,
    # and make the match fail (as in .get_offset_tzone above)

  m ? ::TZInfo::Timezone.get(m[1]) : nil
end
make_from_array(a, zone) click to toggle source
# File lib/et-orbi/make.rb, line 108
def make_from_array(a, zone)

  parse(
    Time.utc(*a).strftime('%Y-%m-%d %H:%M:%S.%6N'), # not a Chronic string
    zone: zone, enable_chronic: false)
end
make_from_date(d, zone) click to toggle source
# File lib/et-orbi/make.rb, line 99
def make_from_date(d, zone)

  make_from_time(
    d.respond_to?(:to_time) ?
    d.to_time :
    Time.parse(d.strftime('%Y-%m-%d %H:%M:%S')),
    zone)
end
make_from_eotime(eot, zone) click to toggle source
# File lib/et-orbi/make.rb, line 125
def make_from_eotime(eot, zone)

  return eot if zone == nil || zone == eot.zone
  EoTime.new(eot.to_f, zone)
end
make_from_numeric(f, zone) click to toggle source
# File lib/et-orbi/make.rb, line 120
def make_from_numeric(f, zone)

  EoTime.new(Time.now.to_f + f, zone)
end
make_from_string(s, zone) click to toggle source
# File lib/et-orbi/make.rb, line 115
def make_from_string(s, zone)

  parse(s, zone: zone)
end
make_from_time(t, zone) click to toggle source
# File lib/et-orbi/make.rb, line 84
def make_from_time(t, zone)

  z =
    zone ||
    get_as_tzone(t) ||
    get_local_tzone(t) ||
    get_tzone(t.zone)

  z ||= t.zone
    # pass the abbreviation anyway,
    # it will be used in the resulting error message

  EoTime.new(t, z)
end
normalize(name) click to toggle source
# File lib/et-orbi/zones.rb, line 176
def normalize(name)

  ZONE_ALIASES[name.sub(/ Daylight /, ' Standard ')]
end
to_offset(n) click to toggle source
# File lib/et-orbi/zone.rb, line 84
def to_offset(n)

  i = n.to_i
  sn = i < 0 ? '-' : '+'; i = i.abs
  hr = i / 3600; mn = i % 3600; sc = i % 60

  sc > 0 ?
    '%s%02d:%02d:%02d' % [ sn, hr, mn, sc ] :
    '%s%02d:%02d' % [ sn, hr, mn ]
end
tz_all() click to toggle source
# File lib/et-orbi/zones.rb, line 241
def tz_all; @tz_all ||= ::TZInfo::Timezone.all; end
tzinfo_data_version() click to toggle source
# File lib/et-orbi/info.rb, line 67
def tzinfo_data_version

  #TZInfo::Data::VERSION rescue nil
  Gem.loaded_specs['tzinfo-data'].version.to_s rescue nil
end
tzinfo_version() click to toggle source
# File lib/et-orbi/info.rb, line 59
def tzinfo_version

  #TZInfo::VERSION
  Gem.loaded_specs['tzinfo'].version.to_s
rescue => err
  err.inspect
end
unzz(name) click to toggle source
# File lib/et-orbi/zones.rb, line 181
def unzz(name)

  m = name.match(/\A([A-Z]{3,4})([+-])(\d{1,2}):?(\d{2})?\z/)
  return nil unless m

  abbs = [ m[1] ]; a = m[1]
  abbs << "#{a}T" if a.size < 4

  off =
    (m[2] == '+' ? 1 : -1) *
    (m[3].to_i * 3600 + (m[4] || '0').to_i * 60)

  t = Time.now
  twin = Time.utc(t.year, 1, 1) # winter
  tsum = Time.utc(t.year, 7, 1) # summer

  tz_all
    .each { |tz|
      abbs.each { |abb|
        per = tz.period_for_utc(twin)
        return tz.name \
          if per.abbreviation.to_s == abb && per.utc_total_offset == off
        per = tz.period_for_utc(tsum)
        return tz.name \
          if per.abbreviation.to_s == abb && per.utc_total_offset == off } }

  nil
end
windows_zone_code_x(zone_name) click to toggle source
# File lib/et-orbi/zone.rb, line 106
def windows_zone_code_x(zone_name)

  a = [ '_' ]
  a.concat(zone_name.split('/')[0, 2].collect { |s| s[0, 1].upcase })
  a << '_' if a.size < 3

  a.join
end