lib/tzinfo/timezone.rb in tzinfo-0.2.2 vs lib/tzinfo/timezone.rb in tzinfo-0.3.0

- old
+ new

@@ -22,27 +22,29 @@ require 'date' require 'tzinfo/country' require 'tzinfo/time_or_datetime' require 'tzinfo/timezone_period' -require 'tzinfo/timezone_period_list' module TZInfo # Indicate a specified time in a local timezone has more than one # possible time in UTC. This happens when switching from daylight savings time # to normal time where the clocks are rolled back. Thrown by period_for_local # and local_to_utc when using an ambiguous time and not specifying any # means to resolve the ambiguity. class AmbiguousTime < StandardError end + # Thrown to indicate that no TimezonePeriod matching a given time could be found. + class PeriodNotFound < StandardError + end + # Thrown by Timezone#get if the identifier given is not valid. class InvalidTimezoneIdentifier < StandardError end - # Thrown if an attempt is made to do a conversion on a timezone created - # with Timezone.new(nil). + # Thrown if an attempt is made to use a timezone created with Timezone.new(nil). class UnknownTimezone < 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 @@ -64,60 +66,118 @@ # Cache of loaded zones by identifier to avoid using require if a zone # has already been loaded. @@loaded_zones = {} + # Whether the timezones index has been loaded yet. + @@index_loaded = false + # Returns a timezone by its identifier (e.g. "Europe/London", # "America/Chicago" or "UTC"). # - # Raises an exception of the timezone couldn't be found. + # Raises InvalidTimezoneIdentifier if the timezone couldn't be found. def self.get(identifier) instance = @@loaded_zones[identifier] - if instance.nil? + unless instance raise InvalidTimezoneIdentifier, 'Invalid identifier' if identifier !~ /^[A-z0-9\+\-_]+(\/[A-z0-9\+\-_]+)*$/ identifier = identifier.gsub(/-/, '__m__').gsub(/\+/, '__p__') begin require "tzinfo/definitions/#{identifier}" m = Definitions identifier.split(/\//).each {|part| m = m.const_get(part) } - instance = m.instance + + info = m.get + + # Could make Timezone subclasses register an interest in an info + # type. Since there are currently only two however, there isn't + # much point. + if info.kind_of?(DataTimezoneInfo) + instance = DataTimezone.new(info) + elsif info.kind_of?(LinkedTimezoneInfo) + instance = LinkedTimezone.new(info) + else + raise InvalidTimezoneIdentifier, "No handler for info type #{info.class}" + end + @@loaded_zones[instance.identifier] = instance rescue LoadError, NameError => e raise InvalidTimezoneIdentifier, e.message end end instance end - # If identifier is nil calls super(), else calls get(identifier). An - # identfier should always be passed in when called externally. + # Returns a proxy for the Timezone with the given identifier. The proxy + # will cause the real timezone to be loaded when an attempt is made to + # find a period or convert a time. get_proxy will not validate the + # identifier. If an invalid identifier is specified, no exception will be + # raised until the proxy is used. + def self.get_proxy(identifier) + TimezoneProxy.new(identifier) + end + + # If identifier is nil calls super(), otherwise calls get. An identfier + # should always be passed in when called externally. def self.new(identifier = nil) if identifier 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. + # Returns an array containing all the available Timezones. + # + # Returns TimezoneProxy objects to avoid the overhead of loading Timezone + # definitions until a conversion is actually required. def self.all - all_country_zones + get_proxies(all_identifiers) 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. + # Returns an array containing the identifiers of all the available + # Timezones. def self.all_identifiers - all_country_zone_identifiers + load_index + Indexes::Timezones.timezones end + # Returns an array containing all the available Timezones that are based + # on data (are not links to other Timezones). + # + # Returns TimezoneProxy objects to avoid the overhead of loading Timezone + # definitions until a conversion is actually required. + def self.all_data_zones + get_proxies(all_data_zone_identifiers) + end + + # Returns an array containing the identifiers of all the available + # Timezones that are based on data (are not links to other Timezones).. + def self.all_data_zone_identifiers + load_index + Indexes::Timezones.data_timezones + end + + # Returns an array containing all the available Timezones that are links + # to other Timezones. + # + # Returns TimezoneProxy objects to avoid the overhead of loading Timezone + # definitions until a conversion is actually required. + def self.all_linked_zones + get_proxies(all_linked_zone_identifiers) + end + + # Returns an array containing the identifiers of all the available + # Timezones that are links to other Timezones. + def self.all_linked_zone_identifiers + load_index + Indexes::Timezones.linked_timezones + 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 @@ -153,27 +213,39 @@ Country.get('US').zone_identifiers end # The identifier of the timezone, e.g. "Europe/Paris". def identifier - 'Unknown' + raise UnknownTimezone, 'TZInfo::Timezone constructed directly' end # An alias for identifier. def name # Don't use alias, as identifier gets overridden. identifier end - # Returns a friendlier version of the idenfitifer. + # Returns a friendlier version of the identifier. def to_s friendly_identifier - end + end - # Returns a friendlier version of the idenfitifer. Set skip_first_part to + # Returns internal object state as a programmer-readable string. + def inspect + "#<#{self.class}: #{identifier}>" + end + + # Returns a friendlier version of the identifier. Set skip_first_part to # omit the first part of the identifier (typically a region name) where # there is more than one part. + # + # For example: + # + # Timezone.get('Europe/Paris').friendly_identifier(false) #=> "Europe - Paris" + # Timezone.get('Europe/Paris').friendly_identifier(true) #=> "Paris" + # Timezone.get('America/Indiana/Knox').friendly_identifier(false) #=> "America - Knox, Indiana" + # Timezone.get('America/Indiana/Knox').friendly_identifier(true) #=> "Knox, Indiana" def friendly_identifier(skip_first_part = false) parts = identifier.split('/') if parts.empty? # shouldn't happen identifier @@ -209,16 +281,22 @@ end # Returns the TimezonePeriod for the given UTC time. utc can either be # a DateTime, Time or integer timestamp (Time.to_i). 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) - periods.period_for_utc(utc) + raise UnknownTimezone, 'TZInfo::Timezone constructed directly' end + # Returns the set of TimezonePeriod instances that are valid for the given + # local time as an array. If you just want a single period, use + # period_for_local instead and specify how ambiguities should be resolved. + # Returns an empty array if no periods are found for the given time. + def periods_for_local(local) + raise UnknownTimezone, 'TZInfo::Timezone constructed directly' + end + # Returns the TimezonePeriod for the given local time. local can either be # a DateTime, Time or integer timestamp (Time.to_i). Any timezone # information in local is ignored (it is treated as a time in the current # timezone). # @@ -226,14 +304,14 @@ # in the transition from standard time to daylight savings time). There are # also local times that have more than one UTC equivalent (e.g. in the # transition from daylight savings time to standard time). # # In the first case (no equivalent UTC time), a PeriodNotFound exception - # will be thrown. + # will be raised. # # In the second case (more than one equivalent UTC time), an AmbiguousTime - # exception will be thrown unless the optional dst parameter or block + # exception will be raised unless the optional dst parameter or block # handles the ambiguity. # # If the ambiguity is due to a transition from daylight savings time to # standard time, the dst parameter can be used to select whether the # daylight savings time or local time is used. For example, @@ -250,14 +328,15 @@ # specified, it is called. The block must take a single parameter - an # array of the periods that need to be resolved. The block can select and # return a single period or return nil or an empty array # to cause an AmbiguousTime exception to be raised. def period_for_local(local, dst = nil) - results = periods.periods_for_local(local) + results = periods_for_local(local) - # by this point, results must contain at least one period - if results.size < 2 + if results.empty? + raise PeriodNotFound + elsif results.size < 2 results.first else # ambiguous result try to resolve if !dst.nil? @@ -274,11 +353,11 @@ results = yield results end if results.is_a?(TimezonePeriod) results - elsif !results.nil? && results.size == 1 + elsif results && results.size == 1 results.first else raise AmbiguousTime, "#{local} is an ambiguous local time." end end @@ -304,14 +383,14 @@ # in the transition from standard time to daylight savings time). There are # also local times that have more than one UTC equivalent (e.g. in the # transition from daylight savings time to standard time). # # In the first case (no equivalent UTC time), a PeriodNotFound exception - # will be thrown. + # will be raised. # # In the second case (more than one equivalent UTC time), an AmbiguousTime - # exception will be thrown unless the optional dst parameter or block + # exception will be raised unless the optional dst parameter or block # handles the ambiguity. # # If the ambiguity is due to a transition from daylight savings time to # standard time, the dst parameter can be used to select whether the # daylight savings time or local time is used. For example, @@ -357,100 +436,71 @@ period = period_for_utc(utc) [period.to_local(utc), period] end alias :current_time_and_period :current_period_and_time - - # Two Timezones are considered to be equal if their identifiers are the same. - def ==(tz) - identifier == tz.identifier + + # Converts a time in UTC to local time and returns it as a string + # according to the given format. The formatting is identical to + # Time.strftime and DateTime.strftime, except %Z is replaced with the + # timezone abbreviation for the specified time (for example, EST or EDT). + def strftime(format, utc = Time.now.utc) + period = period_for_utc(utc) + local = period.to_local(utc) + local = Time.at(local).utc unless local.kind_of?(Time) || local.kind_of?(DateTime) + abbreviation = period.abbreviation.to_s.gsub(/%/, '%%') + + format = format.gsub(/(.?)%Z/) do + if $1 == '%' + # return %%Z so the real strftime treats it as a literal %Z too + '%%Z' + else + "#$1#{abbreviation}" + end + end + + local.strftime(format) end - # Compare two Timezones based on their identifier. Returns -1 if tz is less + # Compares 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 - def periods #:nodoc: - raise UnknownTimezone, 'An attempt was made to perform a conversion on ' + - 'an unknown timezone (i.e. one created with TZInfo::Timezone.new(nil))' + # Returns true if and only if the identifier of tz is equal to the + # identifier of this Timezone. + def eql?(tz) + self == tz end - protected - def self.setup - class_eval <<CODE - @@periods = TimezonePeriodList.new - @instance = new - - def identifier - self.class.get_identifier - end - - def self.instance - @instance - end - - def periods - @@periods - end - - protected - def self.add_period(year, month) - @@periods.add(year, month) { yield } - end - - def self.add_unbounded_start_period - @@periods.add_unbounded_start { yield } - end - - def self.set_identifier(identifier) - @identifier = identifier - end - - def self.get_identifier - @identifier - end -CODE - end - - def self.setup_linked - class_eval <<CODE - @instance = new -CODE - end - end - - # A proxy class representing a timezone with a given identifier. It can be - # constructed with an identifier and behaves almost identically to a Timezone - # loaded through Timezone.get. The first time an attempt is made to perform - # a conversion on the proxy, the real Timezone class is loaded. If the - # proxy's identifier was not valid, then an exception will be thrown at this - # point. - class TimezoneProxy < Timezone - # Construct a new TimezoneProxy for the given identifier. The identifier - # is not checked when constructing the proxy. It will be validated on the - # first conversion. - def self.new(identifier) - # Need to override new to undo the behaviour introduced in Timezone#new. - tzp = super() - tzp.instance_eval <<CODE - @identifier = identifier - @real_tz = nil -CODE - tzp + # Returns a hash of this Timezone. + def hash + identifier.hash end - - # The identifier of the timezone, e.g. "Europe/Paris". - def identifier - @real_tz.nil? ? @identifier : @real_tz.identifier + + # Dumps this Timezone for marshalling. + def _dump(limit) + identifier end - def periods #:nodoc: - if @real_tz.nil? - # We now need the actual data. Load in the real timezone. - @real_tz = Timezone.get(@identifier) + # Loads a marshalled Timezone. + def self._load(data) + Timezone.get(data) + end + + private + # Loads in the index of timezones if it hasn't already been loaded. + def self.load_index + unless @@index_loaded + require 'tzinfo/indexes/timezones' + @@index_loaded = true + end end - @real_tz.periods - end - end + + # Returns an array of proxies corresponding to the given array of + # identifiers. + def self.get_proxies(identifiers) + identifiers.collect {|identifier| get_proxy(identifier)} + end + end end