File: active_support/time_with_zone.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: ActiveSupport#3
  class: TimeWithZone#33
includes
  Comparable ( Builtin-Module )
inherits from
  Object ( Builtin-Module )
has properties
attribute: time_zone [R] #35
method: initialize / 4 #37
method: time #43
method: utc #48
method: period #57
method: in_time_zone / 1 #62
method: localtime #68
method: dst? #73
method: utc? #78
method: utc_offset #83
method: formatted_offset / 2 #89
method: zone #94
method: inspect #98
method: xmlschema / 1 #102
method: as_json / 1 #124
method: to_yaml / 1 #132
method: httpdate #140
method: rfc2822 #144
method: to_s / 1 #151
method: strftime / 1 #163
method: <=> / 1 #169
method: between? / 2 #173
method: past? #177
method: today? #181
method: future? #185
method: eql? / 1 #189
method: + / 1 #193
method: - / 1 #204
method: since / 1 #217
method: ago / 1 #227
method: advance / 1 #231
method: usec #249
method: to_a #253
method: to_f #257
method: to_i #261
method: to_time #268
method: to_datetime #272
method: acts_like_time? #277
method: is_a? / 1 #282
method: freeze #287
method: marshal_dump #292
method: marshal_load / 1 #296
method: respond_to? / 2 #301
method: method_missing / 3 #308
method: get_period_and_ensure_valid_local_time #314
method: transfer_time_values_to_utc_constructor / 1 #327
method: duration_of_variable_length? / 1 #331

Class Hierarchy

Code

   1  require 'tzinfo'
   2 
   3  module ActiveSupport
   4    # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are
   5    # limited to UTC and the system's <tt>ENV['TZ']</tt> zone.
   6    #
   7    # You shouldn't ever need to create a TimeWithZone instance directly via <tt>new</tt> -- instead, Rails provides the methods
   8    # +local+, +parse+, +at+ and +now+ on TimeZone instances, and +in_time_zone+ on Time and DateTime instances, for a more
   9    # user-friendly syntax. Examples:
  10    #
  11    #   Time.zone = 'Eastern Time (US & Canada)'        # => 'Eastern Time (US & Canada)'
  12    #   Time.zone.local(2007, 2, 10, 15, 30, 45)        # => Sat, 10 Feb 2007 15:30:45 EST -05:00
  13    #   Time.zone.parse('2007-02-01 15:30:45')          # => Sat, 10 Feb 2007 15:30:45 EST -05:00
  14    #   Time.zone.at(1170361845)                        # => Sat, 10 Feb 2007 15:30:45 EST -05:00
  15    #   Time.zone.now                                   # => Sun, 18 May 2008 13:07:55 EDT -04:00
  16    #   Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone  # => Sat, 10 Feb 2007 15:30:45 EST -05:00
  17    #
  18    # See TimeZone and ActiveSupport::CoreExtensions::Time::Zones for further documentation for these methods.
  19    #
  20    # TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangable. Examples:
  21    #
  22    #   t = Time.zone.now                     # => Sun, 18 May 2008 13:27:25 EDT -04:00
  23    #   t.hour                                # => 13
  24    #   t.dst?                                # => true
  25    #   t.utc_offset                          # => -14400
  26    #   t.zone                                # => "EDT"
  27    #   t.to_s(:rfc822)                       # => "Sun, 18 May 2008 13:27:25 -0400"
  28    #   t + 1.day                             # => Mon, 19 May 2008 13:27:25 EDT -04:00
  29    #   t.beginning_of_year                   # => Tue, 01 Jan 2008 00:00:00 EST -05:00
  30    #   t > Time.utc(1999)                    # => true
  31    #   t.is_a?(Time)                         # => true
  32    #   t.is_a?(ActiveSupport::TimeWithZone)  # => true
  33    class TimeWithZone
  34      include Comparable
  35      attr_reader :time_zone
  36 
  37      def initialize(utc_time, time_zone, local_time = nil, period = nil)
  38        @utc, @time_zone, @time = utc_time, time_zone, local_time
  39        @period = @utc ? period : get_period_and_ensure_valid_local_time
  40      end
  41 
  42      # Returns a Time or DateTime instance that represents the time in +time_zone+.
  43      def time
  44        @time ||= period.to_local(@utc)
  45      end
  46 
  47      # Returns a Time or DateTime instance that represents the time in UTC.
  48      def utc
  49        @utc ||= period.to_utc(@time)
  50      end
  51      alias_method :comparable_time, :utc
  52      alias_method :getgm, :utc
  53      alias_method :getutc, :utc
  54      alias_method :gmtime, :utc
  55 
  56      # Returns the underlying TZInfo::TimezonePeriod.
  57      def period
  58        @period ||= time_zone.period_for_utc(@utc)
  59      end
  60 
  61      # Returns the simultaneous time in <tt>Time.zone</tt>, or the specified zone.
  62      def in_time_zone(new_zone = ::Time.zone)
  63        return self if time_zone == new_zone
  64        utc.in_time_zone(new_zone)
  65      end
  66 
  67      # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your system's <tt>ENV['TZ']</tt> zone
  68      def localtime
  69        utc.getlocal
  70      end
  71      alias_method :getlocal, :localtime
  72 
  73      def dst?
  74        period.dst?
  75      end
  76      alias_method :isdst, :dst?
  77 
  78      def utc?
  79        time_zone.name == 'UTC'
  80      end
  81      alias_method :gmt?, :utc?
  82 
  83      def utc_offset
  84        period.utc_total_offset
  85      end
  86      alias_method :gmt_offset, :utc_offset
  87      alias_method :gmtoff, :utc_offset
  88 
  89      def formatted_offset(colon = true, alternate_utc_string = nil)
  90        utc? && alternate_utc_string || utc_offset.to_utc_offset_s(colon)
  91      end
  92 
  93      # Time uses +zone+ to display the time zone abbreviation, so we're duck-typing it.
  94      def zone
  95        period.zone_identifier.to_s
  96      end
  97 
  98      def inspect
  99        "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
 100      end
 101 
 102      def xmlschema(fraction_digits = 0)
 103        fraction = if fraction_digits > 0
 104          ".%i" % time.usec.to_s[0, fraction_digits]
 105        end
 106 
 107        "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
 108      end
 109      alias_method :iso8601, :xmlschema
 110 
 111      # Coerces the date to a string for JSON encoding.
 112      #
 113      # ISO 8601 format is used if ActiveSupport::JSON::Encoding.use_standard_json_time_format is set.
 114      #
 115      # ==== Examples
 116      #
 117      #   # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
 118      #   Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
 119      #   # => "2005-02-01T15:15:10Z"
 120      #
 121      #   # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
 122      #   Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
 123      #   # => "2005/02/01 15:15:10 +0000"
 124      def as_json(options = nil)
 125        if ActiveSupport::JSON::Encoding.use_standard_json_time_format
 126          xmlschema
 127        else
 128          %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
 129        end
 130      end
 131 
 132      def to_yaml(options = {})
 133        if options.kind_of?(YAML::Emitter)
 134          utc.to_yaml(options)
 135        else
 136          time.to_yaml(options).gsub('Z', formatted_offset(true, 'Z'))
 137        end
 138      end
 139 
 140      def httpdate
 141        utc.httpdate
 142      end
 143 
 144      def rfc2822
 145        to_s(:rfc822)
 146      end
 147      alias_method :rfc822, :rfc2822
 148 
 149      # <tt>:db</tt> format outputs time in UTC; all others output time in local.
 150      # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly.
 151      def to_s(format = :default)
 152        return utc.to_s(format) if format == :db
 153        if formatter = ::Time::DATE_FORMATS[format]
 154          formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
 155        else
 156          "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
 157        end
 158      end
 159      alias_method :to_formatted_s, :to_s
 160 
 161      # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and +formatted_offset+, respectively, before passing to
 162      # Time#strftime, so that zone information is correct
 163      def strftime(format)
 164        format = format.gsub('%Z', zone).gsub('%z', formatted_offset(false))
 165        time.strftime(format)
 166      end
 167 
 168      # Use the time in UTC for comparisons.
 169      def <=>(other)
 170        utc <=> other
 171      end
 172 
 173      def between?(min, max)
 174        utc.between?(min, max)
 175      end
 176 
 177      def past?
 178        utc.past?
 179      end
 180 
 181      def today?
 182        time.today?
 183      end
 184 
 185      def future?
 186        utc.future?
 187      end
 188 
 189      def eql?(other)
 190        utc == other
 191      end
 192 
 193      def +(other)
 194        # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
 195        # otherwise move forward from #utc, for accuracy when moving across DST boundaries
 196        if duration_of_variable_length?(other)
 197          method_missing(:+, other)
 198        else
 199          result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
 200          result.in_time_zone(time_zone)
 201        end
 202      end
 203 
 204      def -(other)
 205        # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
 206        # otherwise move backwards #utc, for accuracy when moving across DST boundaries
 207        if other.acts_like?(:time)
 208          utc.to_f - other.to_f
 209        elsif duration_of_variable_length?(other)
 210          method_missing(:-, other)
 211        else
 212          result = utc.acts_like?(:date) ? utc.ago(other) : utc - other rescue utc.ago(other)
 213          result.in_time_zone(time_zone)
 214        end
 215      end
 216 
 217      def since(other)
 218        # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
 219        # otherwise move forward from #utc, for accuracy when moving across DST boundaries
 220        if duration_of_variable_length?(other)
 221          method_missing(:since, other)
 222        else
 223          utc.since(other).in_time_zone(time_zone)
 224        end
 225      end
 226 
 227      def ago(other)
 228        since(-other)
 229      end
 230 
 231      def advance(options)
 232        # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time,
 233        # otherwise advance from #utc, for accuracy when moving across DST boundaries
 234        if options.values_at(:years, :weeks, :months, :days).any?
 235          method_missing(:advance, options)
 236        else
 237          utc.advance(options).in_time_zone(time_zone)
 238        end
 239      end
 240 
 241      %w(year mon month day mday wday yday hour min sec to_date).each do |method_name|
 242        class_eval <<-EOV
 243          def #{method_name}     # def year
 244            time.#{method_name}  #   time.year
 245          end                    # end
 246        EOV
 247      end
 248 
 249      def usec
 250        time.respond_to?(:usec) ? time.usec : 0
 251      end
 252 
 253      def to_a
 254        [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
 255      end
 256 
 257      def to_f
 258        utc.to_f
 259      end
 260 
 261      def to_i
 262        utc.to_i
 263      end
 264      alias_method :hash, :to_i
 265      alias_method :tv_sec, :to_i
 266 
 267      # A TimeWithZone acts like a Time, so just return +self+.
 268      def to_time
 269        self
 270      end
 271 
 272      def to_datetime
 273        utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
 274      end
 275 
 276      # So that +self+ <tt>acts_like?(:time)</tt>.
 277      def acts_like_time?
 278        true
 279      end
 280 
 281      # Say we're a Time to thwart type checking.
 282      def is_a?(klass)
 283        klass == ::Time || super
 284      end
 285      alias_method :kind_of?, :is_a?
 286 
 287      def freeze
 288        period; utc; time # preload instance variables before freezing
 289        super
 290      end
 291 
 292      def marshal_dump
 293        [utc, time_zone.name, time]
 294      end
 295 
 296      def marshal_load(variables)
 297        initialize(variables[0].utc, ::Time.__send__(:get_zone, variables[1]), variables[2].utc)
 298      end
 299 
 300      # Ensure proxy class responds to all methods that underlying time instance responds to.
 301      def respond_to?(sym, include_priv = false)
 302        # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime
 303        return false if sym.to_s == 'acts_like_date?'
 304        super || time.respond_to?(sym, include_priv)
 305      end
 306 
 307      # Send the missing method to +time+ instance, and wrap result in a new TimeWithZone with the existing +time_zone+.
 308      def method_missing(sym, *args, &block)
 309        result = time.__send__(sym, *args, &block)
 310        result.acts_like?(:time) ? self.class.new(nil, time_zone, result) : result
 311      end
 312 
 313      private
 314        def get_period_and_ensure_valid_local_time
 315          # we don't want a Time.local instance enforcing its own DST rules as well,
 316          # so transfer time values to a utc constructor if necessary
 317          @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
 318          begin
 319            @time_zone.period_for_local(@time)
 320          rescue ::TZInfo::PeriodNotFound
 321            # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
 322            @time += 1.hour
 323            retry
 324          end
 325        end
 326 
 327        def transfer_time_values_to_utc_constructor(time)
 328          ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0)
 329        end
 330 
 331        def duration_of_variable_length?(obj)
 332          ActiveSupport::Duration === obj && obj.parts.any? {|p| [:years, :months, :days].include? p[0] }
 333        end
 334    end
 335  end