Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activesupport/lib/active_support/time_with_zone.rb

Revision 9208, 6.9 kB (checked in by gbuesing, 1 month ago)

TimeWithZone#method_missing: send to utc to advance with dst correctness, otherwise send to time. Adding tests for time calculations methods

Line 
1 require 'tzinfo'
2 module ActiveSupport
3   # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are
4   # limited to UTC and the system's ENV['TZ'] zone
5   class TimeWithZone
6     include Comparable
7     attr_reader :time_zone
8  
9     def initialize(utc_time, time_zone, local_time = nil, period = nil)
10       @utc, @time_zone, @time = utc_time, time_zone, local_time
11       @period = @utc ? period : get_period_and_ensure_valid_local_time
12     end
13  
14     # Returns a Time or DateTime instance that represents the time in time_zone
15     def time
16       @time ||= period.to_local(@utc)
17     end
18
19     # Returns a Time or DateTime instance that represents the time in UTC
20     def utc
21       @utc ||= period.to_utc(@time)
22     end
23     alias_method :comparable_time, :utc
24     alias_method :getgm, :utc
25     alias_method :getutc, :utc
26     alias_method :gmtime, :utc
27  
28     # Returns the underlying TZInfo::TimezonePeriod
29     def period
30       @period ||= time_zone.period_for_utc(@utc)
31     end
32
33     # Returns the simultaneous time in Time.zone, or the specified zone
34     def in_time_zone(new_zone = ::Time.zone)
35       return self if time_zone == new_zone
36       utc.in_time_zone(new_zone)
37     end
38  
39     # Returns a Time.local() instance of the simultaneous time in your system's ENV['TZ'] zone
40     def localtime
41       utc.getlocal
42     end
43     alias_method :getlocal, :localtime
44  
45     def dst?
46       period.dst?
47     end
48     alias_method :isdst, :dst?
49  
50     def utc?
51       time_zone.name == 'UTC'
52     end
53     alias_method :gmt?, :utc?
54  
55     def utc_offset
56       period.utc_total_offset
57     end
58     alias_method :gmt_offset, :utc_offset
59     alias_method :gmtoff, :utc_offset
60  
61     def formatted_offset(colon = true, alternate_utc_string = nil)
62       utc? && alternate_utc_string || utc_offset.to_utc_offset_s(colon)
63     end
64  
65     # Time uses #zone to display the time zone abbreviation, so we're duck-typing it
66     def zone
67       period.zone_identifier.to_s
68     end
69  
70     def inspect
71       "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
72     end
73
74     def xmlschema
75       "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
76     end
77     alias_method :iso8601, :xmlschema
78  
79     def to_json(options = nil)
80       %("#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}")
81     end
82    
83     def to_yaml(options = {})
84       if options.kind_of?(YAML::Emitter)
85         utc.to_yaml(options)
86       else
87         time.to_yaml(options).gsub('Z', formatted_offset(true, 'Z'))
88       end
89     end
90    
91     def httpdate
92       utc.httpdate
93     end
94  
95     def rfc2822
96       to_s(:rfc822)
97     end
98     alias_method :rfc822, :rfc2822
99  
100     # :db format outputs time in UTC; all others output time in local. Uses TimeWithZone's strftime, so %Z and %z work correctly
101     def to_s(format = :default)
102       return utc.to_s(format) if format == :db
103       if formatter = ::Time::DATE_FORMATS[format]
104         formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
105       else
106         "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
107       end
108     end
109    
110     # Replaces %Z and %z directives with #zone and #formatted_offset, respectively, before passing to
111     # Time#strftime, so that zone information is correct
112     def strftime(format)
113       format = format.gsub('%Z', zone).gsub('%z', formatted_offset(false))
114       time.strftime(format)
115     end
116  
117     # Use the time in UTC for comparisons
118     def <=>(other)
119       utc <=> other
120     end
121    
122     def between?(min, max)
123       utc.between?(min, max)
124     end
125    
126     def eql?(other)
127       utc == other
128     end
129    
130     # If wrapped #time is a DateTime, use DateTime#since instead of #+
131     # Otherwise, just pass on to #method_missing
132     def +(other)
133       time.acts_like?(:date) ? method_missing(:since, other) : method_missing(:+, other)
134     end
135    
136     # If a time-like object is passed in, compare it with #utc
137     # Else if wrapped #time is a DateTime, use DateTime#ago instead of #-
138     # Otherwise, just pass on to method missing
139     def -(other)
140       if other.acts_like?(:time)
141         utc - other
142       else
143         time.acts_like?(:date) ? method_missing(:ago, other) : method_missing(:-, other)
144       end
145     end
146    
147     def usec
148       time.respond_to?(:usec) ? time.usec : 0
149     end
150    
151     def to_a
152       [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
153     end
154    
155     def to_f
156       utc.to_f
157     end   
158    
159     def to_i
160       utc.to_i
161     end
162     alias_method :hash, :to_i
163     alias_method :tv_sec, :to_i
164  
165     # A TimeWithZone acts like a Time, so just return self
166     def to_time
167       self
168     end
169    
170     def to_datetime
171       utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
172     end
173    
174     # so that self acts_like?(:time)
175     def acts_like_time?
176       true
177     end
178  
179     # Say we're a Time to thwart type checking
180     def is_a?(klass)
181       klass == ::Time || super
182     end
183     alias_method :kind_of?, :is_a?
184  
185     # Neuter freeze because freezing can cause problems with lazy loading of attributes
186     def freeze
187       self
188     end
189
190     def marshal_dump
191       [utc, time_zone.name, time]
192     end
193    
194     def marshal_load(variables)
195       initialize(variables[0], ::Time.send!(:get_zone, variables[1]), variables[2])
196     end
197  
198     # Ensure proxy class responds to all methods that underlying time instance responds to
199     def respond_to?(sym)
200       # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime
201       return false if sym.to_s == 'acts_like_date?'
202       super || time.respond_to?(sym)
203     end
204  
205     # Send the missing method to time instance, and wrap result in a new TimeWithZone with the existing time_zone
206     def method_missing(sym, *args, &block)
207       if %w(+ - since ago advance).include?(sym.to_s)
208         result = utc.__send__(sym, *args, &block)
209         result.acts_like?(:time) ? result.in_time_zone(time_zone) : result
210       else
211         result = time.__send__(sym, *args, &block)
212         result.acts_like?(:time) ? self.class.new(nil, time_zone, result) : result
213       end
214     end
215    
216     private     
217       def get_period_and_ensure_valid_local_time
218         # we don't want a Time.local instance enforcing its own DST rules as well,
219         # so transfer time values to a utc constructor if necessary
220         @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
221         begin
222           @time_zone.period_for_local(@time)
223         rescue ::TZInfo::PeriodNotFound
224           # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
225           @time += 1.hour
226           retry
227         end
228       end
229      
230       def transfer_time_values_to_utc_constructor(time)
231         ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0)
232       end
233   end
234 end
Note: See TracBrowser for help on using the browser.