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