require 'tzinfo' require 'forwardable' module Teasy # rubocop:disable Metrics/ClassLength class TimeWithZone extend Forwardable include Comparable def_delegators :time, :year, :mon, :month, :day, :hour, :min, :minute, :sec, :usec, :nsec, :subsec, :mday, :wday, :yday, :monday?, :tuesday?, :wednesday?, :thursday?, :friday?, :saturday?, :sunday? def_delegators :period, :dst? def_delegator :period, :utc_total_offset, :utc_offset def_delegators :to_time, :to_i, :to_r, :to_f # rubocop:disable Metrics/ParameterLists def initialize(year, month = nil, day = nil, hour = nil, minute = nil, second = nil, usec_with_frac = nil, zone = Teasy.default_zone) @zone = TZInfo::Timezone.get(zone) @time = Time.utc(year, month, day, hour, minute, second, usec_with_frac) @period = @zone.period_for_local(@time) end # rubocop:enable Metrics/ParameterLists def self.from_time(time, zone = Teasy.default_zone) new(time.year, time.mon, time.day, time.hour, time.min, time.sec, time.nsec / 1_000.0, zone) end def self.from_utc(utc_time, zone = Teasy.default_zone) from_time(utc_time, 'UTC').in_time_zone!(zone) end def in_time_zone!(zone = Teasy.default_zone) time = to_time @zone = TZInfo::Timezone.get(zone) @time = @zone.utc_to_local(time) @period = @zone.period_for_utc(time) self end def in_time_zone(zone = Teasy.default_zone) dup.in_time_zone!(zone) end def zone @zone.identifier end def utc? @zone.identifier == 'UTC' end def utc! @time = @zone.local_to_utc(@time) @zone = TZInfo::Timezone.get('UTC') @period = @zone.period_for_local(@time) self end def utc dup.utc! end def round!(*args) @time = @time.round(*args) self end def round(*args) dup.round!(*args) end def inspect format = utc? ? '%Y-%m-%d %H:%M:%S %Z' : '%Y-%m-%d %H:%M:%S %z' strftime(format) end alias_method :to_s, :inspect def strftime(format) format = replace_zone_info(format) if includes_zone_directive?(format) time.strftime(format) end def asctime strftime('%a %b %e %T %Y') end alias_method :ctime, :asctime def +(other) TimeWithZone.from_utc(to_time + other, @zone.identifier) end def -(other) if other.is_a? Numeric TimeWithZone.from_utc(to_time - other, @zone.identifier) elsif other.respond_to? :to_time to_time - other.to_time else fail TypeError, "#{other.class} can't be coerced into TimeWithZone" end end def <=>(other) return nil unless other.respond_to? :to_time to_time <=> other.to_time end def eql?(other) hash == other.hash end def hash (utc.to_a << self.class).hash end def to_a time.to_a[0..7] + [dst?, period.abbreviation.to_s] end def to_time @utc_time ||= @zone.local_to_utc(@time) end private attr_reader :time, :period # matches valid format directives for zones ZONE_ABBREV = /(?