Updated README.rdoc again
[feedcatcher.git] / vendor / rails / activesupport / lib / active_support / time_with_zone.rb
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 # Returns a JSON string representing the TimeWithZone. If ActiveSupport.use_standard_json_time_format is set to
112 # true, the ISO 8601 format is used.
113 #
114 # ==== Examples
115 #
116 # # With ActiveSupport.use_standard_json_time_format = true
117 # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
118 # # => "2005-02-01T15:15:10Z"
119 #
120 # # With ActiveSupport.use_standard_json_time_format = false
121 # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
122 # # => "2005/02/01 15:15:10 +0000"
123 def to_json(options = nil)
124 if ActiveSupport.use_standard_json_time_format
125 xmlschema.inspect
126 else
127 %("#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}")
128 end
129 end
130
131 def to_yaml(options = {})
132 if options.kind_of?(YAML::Emitter)
133 utc.to_yaml(options)
134 else
135 time.to_yaml(options).gsub('Z', formatted_offset(true, 'Z'))
136 end
137 end
138
139 def httpdate
140 utc.httpdate
141 end
142
143 def rfc2822
144 to_s(:rfc822)
145 end
146 alias_method :rfc822, :rfc2822
147
148 # <tt>:db</tt> format outputs time in UTC; all others output time in local.
149 # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly.
150 def to_s(format = :default)
151 return utc.to_s(format) if format == :db
152 if formatter = ::Time::DATE_FORMATS[format]
153 formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
154 else
155 "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
156 end
157 end
158 alias_method :to_formatted_s, :to_s
159
160 # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and +formatted_offset+, respectively, before passing to
161 # Time#strftime, so that zone information is correct
162 def strftime(format)
163 format = format.gsub('%Z', zone).gsub('%z', formatted_offset(false))
164 time.strftime(format)
165 end
166
167 # Use the time in UTC for comparisons.
168 def <=>(other)
169 utc <=> other
170 end
171
172 def between?(min, max)
173 utc.between?(min, max)
174 end
175
176 def past?
177 utc.past?
178 end
179
180 def today?
181 time.today?
182 end
183
184 def future?
185 utc.future?
186 end
187
188 def eql?(other)
189 utc == other
190 end
191
192 def +(other)
193 # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
194 # otherwise move forward from #utc, for accuracy when moving across DST boundaries
195 if duration_of_variable_length?(other)
196 method_missing(:+, other)
197 else
198 result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
199 result.in_time_zone(time_zone)
200 end
201 end
202
203 def -(other)
204 # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
205 # otherwise move backwards #utc, for accuracy when moving across DST boundaries
206 if other.acts_like?(:time)
207 utc.to_f - other.to_f
208 elsif duration_of_variable_length?(other)
209 method_missing(:-, other)
210 else
211 result = utc.acts_like?(:date) ? utc.ago(other) : utc - other rescue utc.ago(other)
212 result.in_time_zone(time_zone)
213 end
214 end
215
216 def since(other)
217 # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
218 # otherwise move forward from #utc, for accuracy when moving across DST boundaries
219 if duration_of_variable_length?(other)
220 method_missing(:since, other)
221 else
222 utc.since(other).in_time_zone(time_zone)
223 end
224 end
225
226 def ago(other)
227 since(-other)
228 end
229
230 def advance(options)
231 # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time,
232 # otherwise advance from #utc, for accuracy when moving across DST boundaries
233 if options.values_at(:years, :weeks, :months, :days).any?
234 method_missing(:advance, options)
235 else
236 utc.advance(options).in_time_zone(time_zone)
237 end
238 end
239
240 %w(year mon month day mday wday yday hour min sec to_date).each do |method_name|
241 class_eval <<-EOV
242 def #{method_name} # def year
243 time.#{method_name} # time.year
244 end # end
245 EOV
246 end
247
248 def usec
249 time.respond_to?(:usec) ? time.usec : 0
250 end
251
252 def to_a
253 [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
254 end
255
256 def to_f
257 utc.to_f
258 end
259
260 def to_i
261 utc.to_i
262 end
263 alias_method :hash, :to_i
264 alias_method :tv_sec, :to_i
265
266 # A TimeWithZone acts like a Time, so just return +self+.
267 def to_time
268 self
269 end
270
271 def to_datetime
272 utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
273 end
274
275 # So that +self+ <tt>acts_like?(:time)</tt>.
276 def acts_like_time?
277 true
278 end
279
280 # Say we're a Time to thwart type checking.
281 def is_a?(klass)
282 klass == ::Time || super
283 end
284 alias_method :kind_of?, :is_a?
285
286 def freeze
287 period; utc; time # preload instance variables before freezing
288 super
289 end
290
291 def marshal_dump
292 [utc, time_zone.name, time]
293 end
294
295 def marshal_load(variables)
296 initialize(variables[0].utc, ::Time.__send__(:get_zone, variables[1]), variables[2].utc)
297 end
298
299 # Ensure proxy class responds to all methods that underlying time instance responds to.
300 def respond_to?(sym, include_priv = false)
301 # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime
302 return false if sym.to_s == 'acts_like_date?'
303 super || time.respond_to?(sym, include_priv)
304 end
305
306 # Send the missing method to +time+ instance, and wrap result in a new TimeWithZone with the existing +time_zone+.
307 def method_missing(sym, *args, &block)
308 result = time.__send__(sym, *args, &block)
309 result.acts_like?(:time) ? self.class.new(nil, time_zone, result) : result
310 end
311
312 private
313 def get_period_and_ensure_valid_local_time
314 # we don't want a Time.local instance enforcing its own DST rules as well,
315 # so transfer time values to a utc constructor if necessary
316 @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
317 begin
318 @time_zone.period_for_local(@time)
319 rescue ::TZInfo::PeriodNotFound
320 # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
321 @time += 1.hour
322 retry
323 end
324 end
325
326 def transfer_time_values_to_utc_constructor(time)
327 ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0)
328 end
329
330 def duration_of_variable_length?(obj)
331 ActiveSupport::Duration === obj && obj.parts.any? {|p| [:years, :months, :days].include? p[0] }
332 end
333 end
334 end