require 'tzinfo/country' module TZInfo class PeriodNotFound < StandardError end # Timezone is the base class of all timezones. It provides a factory method # get to access timezones by identifier. Once a specific Timezone has been # retrieved, DateTimes and Times can be converted between the UTC and the # local time for the zone. For example: # # tz = TZInfo::Timezone.get('America/New_York') # puts tz.utc_to_local(DateTime.new(2005,8,29,15,35,0)).to_s # puts tz.local_to_utc(Time.utc(2005,8,29,11,35,0)).to_s # # The timezone information all comes from the tz database # (see http://www.twinsun.com/tz/tz-link.htm) class Timezone include Comparable # Returns a timezone by its identifier (e.g. "Europe/London", # "America/Chicago" or "UTC"). # # Raises an exception of the timezone couldn't be found. def self.get(identifier) raise 'Invalid identifier' if identifier !~ /^[A-z0-9\+\-_]+(\/[A-z0-9\+\-_]+)*$/ identifier = identifier.gsub(/-/, '__m__').gsub(/\+/, '__p__') require "tzinfo/definitions/#{identifier}" m = Definitions identifier.split(/\//).each {|part| m = m.const_get(part) } m.instance end # If identifier is nil calls super(), else calls get(identifier). def self.new(identifier = nil) if identifier puts 'getting' get(identifier) else super() end end # At the moment, returns the result of all_country_zones. May be changed # in the future to return all the Timezone instances including # non-country specific zones. def self.all all_country_zones end # At the moment, returns the result of all_country_zone_identifiers. May be changed # in the future to return all the zone identifiers including # non-country specific zones. def self.all_identifiers all_country_zone_identifiers end # Returns all the Timezones defined for all Countries. This is not the # complete set of Timezones as some are not country specific (e.g. # 'Etc/GMT'). # # Returns TimezoneProxy objects to avoid the overhead of loading Timezone # definitions until a conversion is actually required. def self.all_country_zones Country.all_codes.inject([]) {|zones,country| zones += Country.get(country).zones } end # Returns all the zone identifiers defined for all Countries. This is not the # complete set of zone identifiers as some are not country specific (e.g. # 'Etc/GMT'). You can obtain a Timezone instance for a given identifier # with the get method. def self.all_country_zone_identifiers Country.all_codes.inject([]) {|zones,country| zones += Country.get(country).zone_identifiers } end # Returns all US Timezone instances. A shortcut for # TZInfo::Country.get('US').zones. # # Returns TimezoneProxy objects to avoid the overhead of loading Timezone # definitions until a conversion is actually required. def self.us_zones Country.get('US').zones end # Returns all US zone identifiers. A shortcut for # TZInfo::Country.get('US').zone_identifiers. def self.us_zone_identifiers Country.get('US').zone_identifiers end # The identifier of the timezone, e.g. "Europe/Paris". def identifier 'Unknown' end # An alias for identifier. def name # Don't use alias, as identifier gets overridden. identifier end # Returns a friendlier version of the idenfitifer. def to_s friendly_identifier end # Returns a friendlier version of the idenfitifer. Set skip_first_part to # omit the first part of the identifier (typically a region name) where # there is more than one part. def friendly_identifier(skip_first_part = false) parts = identifier.split('/') if parts.empty? # shouldn't happen identifier elsif parts.length == 1 parts[0] else if skip_first_part result = '' else result = parts[0] + ' - ' end parts[1, parts.length - 1].reverse_each {|part| part.gsub!(/_/, ' ') # Missing a space if a lower case followed by an upper case and the # name isn't McXxxx. part.gsub!(/[^M]([a-z])([A-Z])/, '\1 \2') part.gsub!(/[M]([a-bd-z])([A-Z])/, '\1 \2') # Missing an apostrophe if two consecutive upper case characters. part.gsub!(/([A-Z])([A-Z])/, '\1\'\2') result << part result << ', ' } result.slice!(result.length - 2, 2) result end end # Returns the TimezonePeriod for the given UTC time. utc can either be # a DateTime or a Time. Any timezone information in utc is ignored (it is # treated as a UTC time). # # If no TimezonePeriod could be found, PeriodNotFound is raised. def period_for_utc(utc) run_on_datetime(utc) {|utc| # dumb search for now periods.each {|period| if period.valid_for_utc?(utc) return period end } # if nothing found, assume the first and last periods are unbounded if periods.length > 0 last = periods[periods.length - 1] if last.utc_after_start?(utc) last else first = periods[0] if first.utc_before_end?(utc) first else raise PeriodNotFound, "No time period found for #{utc}" end end else raise PeriodNotFound, "No time period found for #{utc}" end } end # Returns the TimezonePeriod for the given local time. local can either be # a DateTime or a Time. Any timezone information in local is ignored (it is # treated as a time in the current timezone). # # If no TimezonePeriod could be found, PeriodNotFound is raised. This will # happen when a change to daylight savings occurs and an hour is skipped. def period_for_local(local) run_on_datetime(local) {|local| # dumb search for now periods.each {|period| if period.valid_for_local?(local) return period end } # if nothing found, assume the first and last periods are unbounded if periods.length > 0 last = periods[periods.length - 1] if last.local_after_start?(local) last else first = periods[0] if first.local_before_end?(local) first else raise PeriodNotFound, "No time period found for #{utc}. This could be because a change to daylight savings time caused an hour to be skipped." end end else raise PeriodNotFound, "No time period found for #{utc}. This could be because a change to daylight savings time caused an hour to be skipped." end } end # Converts a time in UTC to the local timezone. utc can either be # a DateTime or a Time. The returned time has the same type as utc. # Any timezone information in utc is ignored (it is treated as a UTC time). def utc_to_local(utc) run_on_datetime(utc) {|utc| period_for_utc(utc).to_local(utc) } end # Converts a time in the local timezone to UTC. local can either be # a DateTime or a Time. The returned time has the same type as local. # Any timezone information in local is ignored (it is treated as a local time). # # During the period when daylight savings reverts to standard time and there # are two possible UTC times for each local time, local_to_utc returns the # earlier time. # # For times skipped during a change to daylight savings, PeriodNotFound is # raised. def local_to_utc(local) run_on_datetime(local) {|local| period_for_local(local).to_utc(local) } end # Returns the current time in the timezone as a Time. def now utc_to_local(Time.now.utc) end # Returns the TimezonePeriod for the current time. def current_period period_for_utc(Time.now.utc) end # Returns the current time and TimezonePeriod as an array. def current_period_and_time utc = Time.now.utc [utc_to_local(utc), period_for_utc(utc)] end # Two Timezones are considered to be equal if their identifiers are the same. def ==(tz) identifier == tz.identifier end # Compare two Timezones based on their identifier. Returns -1 if tz is less # than self, 0 if tz is equal to self and +1 if tz is greater than self. def <=>(tz) identifier <=> tz.identifier end protected def self.setup class_eval < utc end # true if this period is valid for the given local DateTime; otherwise false. def valid_for_local?(local) local_after_start?(local) && local_before_end?(local) end # true if the given local DateTime is after the start of the period; otherwise false. def local_after_start?(local) @local_start.nil? || @local_start <= local end # true if the given local DateTime is before the end of the period; otherwise false. def local_before_end?(local) @local_end.nil? || @local_end > local end # Converts a UTC DateTime to local time based on the offset of this period. def to_local(utc) utc + utc_total_offset_rational end # Converts a local DateTime to UTC based on the offset of this period. def to_utc(local) local - utc_total_offset_rational end end end