lib/et-orbi.rb in et-orbi-1.1.6 vs lib/et-orbi.rb in et-orbi-1.1.7
- old
+ new
@@ -2,16 +2,17 @@
require 'date' if RUBY_VERSION < '1.9.0'
require 'time'
require 'tzinfo'
+require 'et-orbi/eo_time'
require 'et-orbi/zone_aliases'
module EtOrbi
- VERSION = '1.1.6'
+ VERSION = '1.1.7'
#
# module methods
class << self
@@ -21,67 +22,44 @@
EoTime.new(Time.now.to_f, zone)
end
def parse(str, opts={})
+ str, str_zone = extract_zone(str)
+
if defined?(::Chronic) && t = ::Chronic.parse(str, opts)
- return EoTime.new(t, nil)
+
+ str = [ t.strftime('%F %T'), str_zone ].compact.join(' ')
end
- #rold = RUBY_VERSION < '1.9.0'
- #rold = RUBY_VERSION < '2.0.0'
begin
DateTime.parse(str)
rescue
fail ArgumentError, "No time information in #{str.inspect}"
- end #if rold
+ 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`
- str_zone = get_tzone(list_iso8601_zones(str).last)
-#p [ :parse, str, str_zone ]
-#p ENV['TZ']
-
-#p [ :parse, :oz, opts[:zone] ]
-#p [ :parse, :sz, str_zone ]
-#p [ :parse, :foz, find_olson_zone(str) ]
-#p [ :parse, :ltz, local_tzone ]
zone =
opts[:zone] ||
- str_zone ||
- find_olson_zone(str) ||
+ get_tzone(str_zone) ||
determine_local_tzone
-#p [ :parse, :zone, zone ]
- str = str.sub(zone.name, '') unless zone.name.match(/\A[-+]/)
- #
- # for 'Sun Nov 18 16:01:00 Asia/Singapore 2012',
- # although where does rufus-scheduler have it from?
-
local = Time.parse(str)
-#p [ :parse, :local, local, local.zone ]
+ secs = zone.local_to_utc(local).to_f
- secs =
- if str_zone
- local.to_f
- else
- zone.local_to_utc(local).to_f
- end
-#p [ :parse, :secs, secs ]
-
EoTime.new(secs, zone)
end
def make_time(*a)
-#p [ :mt, a ]
zone = a.length > 1 ? get_tzone(a.last) : nil
a.pop if zone
-#p [ :mt, zone ]
o = a.length > 1 ? a : a.first
-#p [ :mt, :o, o ]
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)
@@ -90,10 +68,11 @@
when ::EtOrbi::EoTime then make_from_eotime(o, zone)
else fail ArgumentError.new(
"Cannot turn #{o.inspect} to a ::EtOrbi::EoTime instance")
end
end
+ alias make make_time
def make_from_time(t, zone)
z =
zone ||
@@ -154,10 +133,11 @@
return nil unless o.is_a?(String)
s = unalias(o)
get_offset_tzone(s) ||
+ get_x_offset_tzone(s) ||
(::TZInfo::Timezone.get(s) rescue nil)
end
def render_nozone_time(seconds)
@@ -172,18 +152,33 @@
tz ? tz.period_for_local(t).abbreviation.to_s : nil
"(secs:#{seconds},utc~:#{ts.inspect},ltz~:#{z.inspect})"
end
+ def tzinfo_version
+
+ #TZInfo::VERSION
+ Gem.loaded_specs['tzinfo'].version.to_s
+ rescue => err
+ err.inspect
+ end
+
+ def tzinfo_data_version
+
+ #TZInfo::Data::VERSION rescue nil
+ Gem.loaded_specs['tzinfo-data'].version.to_s rescue nil
+ end
+
def platform_info
etos = Proc.new { |k, v| "#{k}:#{v.inspect}" }
h = {
'etz' => ENV['TZ'],
'tnz' => Time.now.zone,
- 'tzid' => defined?(TZInfo::Data),
+ '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),
@@ -192,517 +187,123 @@
'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
- alias make make_time
-
# For `make info`
#
def _make_info
puts render_nozone_time(Time.now.to_f)
puts platform_info
end
- protected
+ ZONES_ISO8601 =
+ %r{
+ (?<=:\d\d)\s*
+ (?:
+ [-+]
+ (?:[0-1][0-9]|2[0-4])
+ (?:(?::)?(?:[0-5][0-9]|60))?
+ (?![-+])
+ |Z
+ )
+ }x
- def get_local_tzone(t)
-
- #lt = local_tzone
- #lp = lt.period_for_local(t)
- #ab = lp.abbreviation.to_s
- #
- #return lt \
- # if ab == t.zone
- #return lt \
- # if ab.match(/\A[-+]\d{2}(:?\d{2})?\z/) && lp.utc_offset == t.utc_offset
- #
- #nil
- #
- # keep that in the fridge for now
-
- 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
-
- def get_as_tzone(t)
-
- t.respond_to?(:time_zone) ? t.time_zone : nil
- end
- end
-
- # Our EoTime class (which quacks like a ::Time).
- #
- # An EoTime instance should respond to most of the methods ::Time instances
- # respond to. If a method is missing, feel free to open an issue to
- # ask (politely) for it. If it makes sense, it'll get added, else
- # a workaround will get suggested.
- # The immediate workaround is to call #to_t on the EoTime instance to get
- # equivalent ::Time instance in the local, current, timezone.
- #
- class EoTime
-
+ # https://en.wikipedia.org/wiki/ISO_8601
+ # Postel's law applies
#
- # class methods
+ def list_iso8601_zones(s)
- class << self
-
- def now(zone=nil)
-
- EtOrbi.now(zone)
- end
-
- def parse(str, opts={})
-
- EtOrbi.parse(str, opts)
- end
-
- def get_tzone(o)
-
- EtOrbi.get_tzone(o)
- end
-
- def local_tzone
-
- EtOrbi.determine_local_tzone
- end
-
- def platform_info
-
- EtOrbi.platform_info
- end
-
- def make(o)
-
- EtOrbi.make_time(o)
- end
-
- def utc(*a)
-
- EtOrbi.make_from_array(a, EtOrbi.get_tzone('UTC'))
- end
-
- def local(*a)
-
- EtOrbi.make_from_array(a, local_tzone)
- end
+ s.scan(ZONES_ISO8601).collect(&:strip)
end
- #
- # instance methods
+ ZONES_OLSON = (
+ TZInfo::Timezone.all.collect { |z| z.name }.sort +
+ (0..12).collect { |i| [ "UTC-#{i}", "UTC+#{i}" ] })
+ .flatten
+ .sort_by(&:size)
+ .reverse
- attr_reader :seconds
- attr_reader :zone
+ def list_olson_zones(s)
- def initialize(s, zone)
+ s = s.dup
- @seconds = s.to_f
- @zone = self.class.get_tzone(zone || :local)
-
- fail ArgumentError.new(
- "Cannot determine timezone from #{zone.inspect}" +
- "\n#{EtOrbi.render_nozone_time(@seconds)}" +
- "\n#{EtOrbi.platform_info.sub(',debian:', ",\ndebian:")}" +
- "\nTry setting `ENV['TZ'] = 'Continent/City'` in your script " +
- "(see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)" +
- (defined?(TZInfo::Data) ? '' : "\nand adding gem 'tzinfo-data'")
- ) unless @zone
-
- @time = nil
- # cache for #to_time result
+ ZONES_OLSON
+ .inject([]) { |a, z|
+ i = s.index(z); next a unless i
+ s[i, z.length] = ''
+ a << z
+ a }
end
- def seconds=(f)
+ def find_olson_zone(str)
- @time = nil
- @seconds = f
+ list_olson_zones(str).each { |s| z = get_tzone(s); return z if z }
+ nil
end
- def zone=(z)
+ def extract_zone(str)
- @time = nil
- @zone = self.class.get_tzone(zone || :current)
- end
+ s = str.dup
- # Returns true if this EoTime instance corresponds to 2 different UTC
- # times.
- # It happens when transitioning from DST to winter time.
- #
- # https://www.timeanddate.com/time/change/usa/new-york?year=2018
- #
- def ambiguous?
+ zs = ZONES_OLSON
+ .inject([]) { |a, z|
+ i = s.index(z); next a unless i
+ a << z
+ s[i, z.length] = ''
+ a }
- @zone.local_to_utc(@zone.utc_to_local(utc))
+ s.gsub!(ZONES_ISO8601) { |m| zs << m.strip; '' } #if zs.empty?
- false
+ zs = zs.sort_by { |z| str.index(z) }
- rescue TZInfo::AmbiguousTime
-
- true
+ [ s.strip, zs.last ]
end
- # Returns this ::EtOrbi::EoTime as a ::Time instance
- # in the current UTC timezone.
- #
- def utc
-
- Time.utc(1970) + @seconds
- end
-
- # Returns true if this ::EtOrbi::EoTime instance timezone is UTC.
- # Returns false else.
- #
- def utc?
-
- %w[ gmt utc zulu etc/gmt etc/utc ].include?(
- @zone.canonical_identifier.downcase)
- end
-
- alias getutc utc
- alias getgm utc
- alias to_utc_time utc
-
- def to_f
-
- @seconds
- end
-
- def to_i
-
- @seconds.to_i
- end
-
- def strftime(format)
-
- format = format.gsub(/%(\/?Z|:{0,2}z)/) { |f| strfz(f) }
-
- to_time.strftime(format)
- end
-
- # Returns this ::EtOrbi::EoTime as a ::Time instance
- # in the current timezone.
- #
- # Has a #to_t alias.
- #
- def to_local_time
-
- Time.at(@seconds)
- end
-
- alias to_t to_local_time
-
- def is_dst?
-
- @zone.period_for_utc(utc).std_offset != 0
- end
- alias isdst is_dst?
-
- def to_debug_s
-
- uo = self.utc_offset
- uos = uo < 0 ? '-' : '+'
- uo = uo.abs
- uoh, uom = [ uo / 3600, uo % 3600 ]
-
- [
- 'ot',
- self.strftime('%Y-%m-%d %H:%M:%S'),
- "%s%02d:%02d" % [ uos, uoh, uom ],
- "dst:#{self.isdst}"
- ].join(' ')
- end
-
- def utc_offset
-
- @zone.period_for_utc(utc).utc_offset
- end
-
- %w[
- year month day wday hour min sec usec asctime
- ].each do |m|
- define_method(m) { to_time.send(m) }
- end
-
- def ==(o)
-
- o.is_a?(EoTime) &&
- o.seconds == @seconds &&
- (o.zone == @zone || o.zone.current_period == @zone.current_period)
- end
- #alias eql? == # FIXME see Object#== (ri)
-
- def >(o); @seconds > _to_f(o); end
- def >=(o); @seconds >= _to_f(o); end
- def <(o); @seconds < _to_f(o); end
- def <=(o); @seconds <= _to_f(o); end
- def <=>(o); @seconds <=> _to_f(o); end
-
- def add(t); @time = nil; @seconds += t.to_f; self; end
- def subtract(t); @time = nil; @seconds -= t.to_f; self; end
-
- def +(t); inc(t, 1); end
- def -(t); inc(t, -1); end
-
- WEEK_S = 7 * 24 * 3600
-
- def monthdays
-
- date = to_time
-
- pos = 1
- d = self.dup
-
- loop do
- d.add(-WEEK_S)
- break if d.month != date.month
- pos = pos + 1
- end
-
- neg = -1
- d = self.dup
-
- loop do
- d.add(WEEK_S)
- break if d.month != date.month
- neg = neg - 1
- end
-
- [ "#{date.wday}##{pos}", "#{date.wday}##{neg}" ]
- end
-
- def to_s
-
- strftime('%Y-%m-%d %H:%M:%S %z')
- end
-
- def to_zs
-
- strftime('%Y-%m-%d %H:%M:%S %/Z')
- end
-
- def iso8601(fraction_digits=0)
-
- s = (fraction_digits || 0) > 0 ? ".%#{fraction_digits}N" : ''
- strftime("%Y-%m-%dT%H:%M:%S#{s}%:z")
- end
-
- # Debug current time by showing local time / delta / utc time
- # for example: "0120-7(0820)"
- #
- def to_utc_comparison_s
-
- per = @zone.period_for_utc(utc)
- off = per.utc_total_offset
-
- off = off / 3600
- off = off >= 0 ? "+#{off}" : off.to_s
-
- strftime('%H%M') + off + utc.strftime('(%H%M)')
- end
-
- def to_time_s
-
- strftime("%H:%M:%S.#{'%06d' % usec}")
- end
-
- def inc(t, dir=1)
-
- case t
- when Numeric
- nt = self.dup
- nt.seconds += dir * t.to_f
- nt
- when ::Time, ::EtOrbi::EoTime
- fail ArgumentError.new(
- "Cannot add #{t.class} to EoTime") if dir > 0
- @seconds + dir * t.to_f
- else
- fail ArgumentError.new(
- "Cannot call add or subtract #{t.class} to EoTime instance")
- end
- end
-
- def localtime(zone=nil)
-
- EoTime.new(self.to_f, zone)
- end
-
- alias translate localtime
-
- def wday_in_month
-
- [ count_weeks(-1), - count_weeks(1) ]
- end
-
- def reach(points)
-
- t = EoTime.new(self.to_f, @zone)
- step = 1
-
- s = points[:second] || points[:sec] || points[:s]
- m = points[:minute] || points[:min] || points[:m]
- h = points[:hour] || points[:hou] || points[:h]
-
- fail ArgumentError.new("missing :second, :minute, and :hour") \
- unless s || m || h
-
- if !s && !m
- step = 60 * 60
- t -= t.sec
- t -= t.min * 60
- elsif !s
- step = 60
- t -= t.sec
- end
-
- loop do
- t += step
- next if s && t.sec != s
- next if m && t.min != m
- next if h && t.hour != h
- break
- end
-
- t
- end
-
- protected
-
- # Returns a Ruby Time instance.
- #
- # Warning: the timezone of that Time instance will be UTC when used with
- # TZInfo < 2.0.0.
- #
- def to_time
-
- @time ||= @zone.utc_to_local(utc)
- end
-
- def count_weeks(dir)
-
- c = 0
- t = self
- until t.month != self.month
- c += 1
- t += dir * (7 * 24 * 3600)
- end
-
- c
- end
-
- def strfz(code)
-
- return @zone.name if code == '%/Z'
-
- per = @zone.period_for_utc(utc)
-
- return per.abbreviation.to_s if code == '%Z'
-
- off = per.utc_total_offset
- #
- sn = off < 0 ? '-' : '+'; off = off.abs
- hr = off / 3600
- mn = (off % 3600) / 60
- sc = 0
-
- if @zone.name == 'UTC'
- 'Z' # align on Ruby ::Time#iso8601
- elsif code == '%z'
- '%s%02d%02d' % [ sn, hr, mn ]
- elsif code == '%:z'
- '%s%02d:%02d' % [ sn, hr, mn ]
- else
- '%s%02d:%02d:%02d' % [ sn, hr, mn, sc ]
- end
- end
-
- def _to_f(o)
-
- fail ArgumentError(
- "Comparison of EoTime with #{o.inspect} failed"
- ) unless o.is_a?(EoTime) || o.is_a?(Time)
-
- o.to_f
- end
- end
-
- class << self
-
- #
- # extra public methods
-
- # https://en.wikipedia.org/wiki/ISO_8601
- # Postel's law applies
- #
- def list_iso8601_zones(s)
-
- s
- .scan(
- %r{
- (?<=:\d\d)
- \s*
- (?:
- [-+]
- (?:[0-1][0-9]|2[0-4])
- (?:(?::)?(?:[0-5][0-9]|60))?
- (?![-+])
- |
- Z
- )
- }x)
- .collect(&:strip)
- end
-
- def list_olson_zones(s)
-
- s
- .scan(
- %r{
- (?<=\s|\A)
- (?:[A-Z][A-Za-z0-9+_-]+)
- (?:\/(?:[A-Z][A-Za-z0-9+_-]+)){0,2}
- }x)
- end
-
- def find_olson_zone(str)
-
- list_olson_zones(str).each { |s| z = get_tzone(s); return z if z }
- nil
- end
-
def determine_local_tzone
+ # ENV has the priority
+
etz = ENV['TZ']
- tz = etz && (::TZInfo::Timezone.get(etz) rescue nil)
+ 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
+ alias zone determine_local_tzone
attr_accessor :_os_zone # test tool
def os_tz
@@ -710,11 +311,13 @@
if defined?(@_os_zone) && @_os_zone
@os_tz ||= (debian_tz || centos_tz || osx_tz)
end
- def to_windows_tz(zone_name, time=Time.now)
+ # Semi-helpful, since it requires the current time
+ #
+ 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)
@@ -727,18 +330,49 @@
abbs = [
tz.period_for_utc(twin).abbreviation.to_s,
tz.period_for_utc(tsum).abbreviation.to_s ]
.uniq
- [ abbs[0], tzop, tzoh, tzos, abbs[1] ].compact.join
+ 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
#
# protected module methods
protected
+ 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
+
+ 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
+
+ # https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
+ #
+ # If it responds to #time_zone, then return that time zone.
+ #
+ def get_as_tzone(t)
+
+ t.respond_to?(:time_zone) ? t.time_zone : nil
+ end
+
def to_offset(n)
i = n.to_i
sn = i < 0 ? '-' : '+'; i = i.abs
hr = i / 3600; mn = i % 3600; sc = i % 60
@@ -746,37 +380,37 @@
sc > 0 ?
'%s%02d:%02d:%02d' % [ sn, hr, mn, sc ] :
'%s%02d:%02d' % [ sn, hr, mn ]
end
+ # custom timezones, no DST, just an offset, like "+08:00" or "-01:30"
+ #
def get_offset_tzone(str)
- # custom timezones, no DST, just an offset, like "+08:00" or "-01:30"
-
- m = str.match(/\A([+-][0-1][0-9]):?([0-5][0-9])?\z/) rescue nil
+ 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
- return (
- (@custom_tz_cache ||= {})[str] =
- create_offset_tzone(hr * 3600 + mn * 60, str)
- ) if hr
-
- nil
+ hr ?
+ custom_tzs[str] = create_offset_tzone(hr * 3600 + mn * 60, str) :
+ nil
end
- if defined? TZInfo::DataSources::ConstantOffsetDataTimezoneInfo
+ if defined?(TZInfo::DataSources::ConstantOffsetDataTimezoneInfo)
# TZInfo >= 2.0.0
def create_offset_tzone(utc_off, id)
off = TZInfo::TimezoneOffset.new(utc_off, 0, id)
@@ -793,10 +427,20 @@
tzi.offset(id, utc_off, 0, id)
tzi.create_timezone
end
end
+ 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
+
def determine_local_tzones
tabbs = (-6..5)
.collect { |i|
t = Time.now + i * 30 * 24 * 3600
@@ -809,14 +453,13 @@
#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_all ||= ::TZInfo::Timezone.all
@tz_winter_summer ||= {}
- @tz_winter_summer[tabbs] ||= @tz_all
+ @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}",
@@ -824,10 +467,13 @@
.uniq.sort.join('|') }
@tz_winter_summer[tabbs]
end
+ def custom_tzs; @custom_tzs ||= {}; end
+ def tz_all; @tz_all ||= ::TZInfo::Timezone.all; end
+
#
# system tz determination
def debian_tz
@@ -861,21 +507,7 @@
def gather_tzs
{ :debian => debian_tz, :centos => centos_tz, :osx => osx_tz }
end
end
-
- #def in_zone(&block)
- #
- # current_timezone = ENV['TZ']
- # ENV['TZ'] = @zone
- #
- # block.call
- #
- #ensure
- #
- # ENV['TZ'] = current_timezone
- #end
- #
- # kept around as a (thread-unsafe) relic
end