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