1d87fa64b5ada4105efaff4b40bc5434fcf5d7fc
[depot.git] / time_zone.rb
1 # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. It allows us to do the following:
2 #
3 # * Limit the set of zones provided by TZInfo to a meaningful subset of 142 zones.
4 # * Retrieve and display zones with a friendlier name (e.g., "Eastern Time (US & Canada)" instead of "America/New_York").
5 # * Lazily load TZInfo::Timezone instances only when they're needed.
6 # * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+, +parse+, +at+ and +now+ methods.
7 #
8 # If you set <tt>config.time_zone</tt> in the Rails Initializer, you can access this TimeZone object via <tt>Time.zone</tt>:
9 #
10 # # environment.rb:
11 # Rails::Initializer.run do |config|
12 # config.time_zone = "Eastern Time (US & Canada)"
13 # end
14 #
15 # Time.zone # => #<TimeZone:0x514834...>
16 # Time.zone.name # => "Eastern Time (US & Canada)"
17 # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00
18 #
19 # The version of TZInfo bundled with Active Support only includes the definitions necessary to support the zones
20 # defined by the TimeZone class. If you need to use zones that aren't defined by TimeZone, you'll need to install the TZInfo gem
21 # (if a recent version of the gem is installed locally, this will be used instead of the bundled version.)
22 module ActiveSupport
23 class TimeZone
24 unless const_defined?(:MAPPING)
25 # Keys are Rails TimeZone names, values are TZInfo identifiers
26 MAPPING = {
27 "International Date Line West" => "Pacific/Midway",
28 "Midway Island" => "Pacific/Midway",
29 "Samoa" => "Pacific/Pago_Pago",
30 "Hawaii" => "Pacific/Honolulu",
31 "Alaska" => "America/Juneau",
32 "Pacific Time (US & Canada)" => "America/Los_Angeles",
33 "Tijuana" => "America/Tijuana",
34 "Mountain Time (US & Canada)" => "America/Denver",
35 "Arizona" => "America/Phoenix",
36 "Chihuahua" => "America/Chihuahua",
37 "Mazatlan" => "America/Mazatlan",
38 "Central Time (US & Canada)" => "America/Chicago",
39 "Saskatchewan" => "America/Regina",
40 "Guadalajara" => "America/Mexico_City",
41 "Mexico City" => "America/Mexico_City",
42 "Monterrey" => "America/Monterrey",
43 "Central America" => "America/Guatemala",
44 "Eastern Time (US & Canada)" => "America/New_York",
45 "Indiana (East)" => "America/Indiana/Indianapolis",
46 "Bogota" => "America/Bogota",
47 "Lima" => "America/Lima",
48 "Quito" => "America/Lima",
49 "Atlantic Time (Canada)" => "America/Halifax",
50 "Caracas" => "America/Caracas",
51 "La Paz" => "America/La_Paz",
52 "Santiago" => "America/Santiago",
53 "Newfoundland" => "America/St_Johns",
54 "Brasilia" => "America/Sao_Paulo",
55 "Buenos Aires" => "America/Argentina/Buenos_Aires",
56 "Georgetown" => "America/Argentina/San_Juan",
57 "Greenland" => "America/Godthab",
58 "Mid-Atlantic" => "Atlantic/South_Georgia",
59 "Azores" => "Atlantic/Azores",
60 "Cape Verde Is." => "Atlantic/Cape_Verde",
61 "Dublin" => "Europe/Dublin",
62 "Edinburgh" => "Europe/Dublin",
63 "Lisbon" => "Europe/Lisbon",
64 "London" => "Europe/London",
65 "Casablanca" => "Africa/Casablanca",
66 "Monrovia" => "Africa/Monrovia",
67 "UTC" => "Etc/UTC",
68 "Belgrade" => "Europe/Belgrade",
69 "Bratislava" => "Europe/Bratislava",
70 "Budapest" => "Europe/Budapest",
71 "Ljubljana" => "Europe/Ljubljana",
72 "Prague" => "Europe/Prague",
73 "Sarajevo" => "Europe/Sarajevo",
74 "Skopje" => "Europe/Skopje",
75 "Warsaw" => "Europe/Warsaw",
76 "Zagreb" => "Europe/Zagreb",
77 "Brussels" => "Europe/Brussels",
78 "Copenhagen" => "Europe/Copenhagen",
79 "Madrid" => "Europe/Madrid",
80 "Paris" => "Europe/Paris",
81 "Amsterdam" => "Europe/Amsterdam",
82 "Berlin" => "Europe/Berlin",
83 "Bern" => "Europe/Berlin",
84 "Rome" => "Europe/Rome",
85 "Stockholm" => "Europe/Stockholm",
86 "Vienna" => "Europe/Vienna",
87 "West Central Africa" => "Africa/Algiers",
88 "Bucharest" => "Europe/Bucharest",
89 "Cairo" => "Africa/Cairo",
90 "Helsinki" => "Europe/Helsinki",
91 "Kyev" => "Europe/Kiev",
92 "Riga" => "Europe/Riga",
93 "Sofia" => "Europe/Sofia",
94 "Tallinn" => "Europe/Tallinn",
95 "Vilnius" => "Europe/Vilnius",
96 "Athens" => "Europe/Athens",
97 "Istanbul" => "Europe/Istanbul",
98 "Minsk" => "Europe/Minsk",
99 "Jerusalem" => "Asia/Jerusalem",
100 "Harare" => "Africa/Harare",
101 "Pretoria" => "Africa/Johannesburg",
102 "Moscow" => "Europe/Moscow",
103 "St. Petersburg" => "Europe/Moscow",
104 "Volgograd" => "Europe/Moscow",
105 "Kuwait" => "Asia/Kuwait",
106 "Riyadh" => "Asia/Riyadh",
107 "Nairobi" => "Africa/Nairobi",
108 "Baghdad" => "Asia/Baghdad",
109 "Tehran" => "Asia/Tehran",
110 "Abu Dhabi" => "Asia/Muscat",
111 "Muscat" => "Asia/Muscat",
112 "Baku" => "Asia/Baku",
113 "Tbilisi" => "Asia/Tbilisi",
114 "Yerevan" => "Asia/Yerevan",
115 "Kabul" => "Asia/Kabul",
116 "Ekaterinburg" => "Asia/Yekaterinburg",
117 "Islamabad" => "Asia/Karachi",
118 "Karachi" => "Asia/Karachi",
119 "Tashkent" => "Asia/Tashkent",
120 "Chennai" => "Asia/Kolkata",
121 "Kolkata" => "Asia/Kolkata",
122 "Mumbai" => "Asia/Kolkata",
123 "New Delhi" => "Asia/Kolkata",
124 "Kathmandu" => "Asia/Katmandu",
125 "Astana" => "Asia/Dhaka",
126 "Dhaka" => "Asia/Dhaka",
127 "Sri Jayawardenepura" => "Asia/Colombo",
128 "Almaty" => "Asia/Almaty",
129 "Novosibirsk" => "Asia/Novosibirsk",
130 "Rangoon" => "Asia/Rangoon",
131 "Bangkok" => "Asia/Bangkok",
132 "Hanoi" => "Asia/Bangkok",
133 "Jakarta" => "Asia/Jakarta",
134 "Krasnoyarsk" => "Asia/Krasnoyarsk",
135 "Beijing" => "Asia/Shanghai",
136 "Chongqing" => "Asia/Chongqing",
137 "Hong Kong" => "Asia/Hong_Kong",
138 "Urumqi" => "Asia/Urumqi",
139 "Kuala Lumpur" => "Asia/Kuala_Lumpur",
140 "Singapore" => "Asia/Singapore",
141 "Taipei" => "Asia/Taipei",
142 "Perth" => "Australia/Perth",
143 "Irkutsk" => "Asia/Irkutsk",
144 "Ulaan Bataar" => "Asia/Ulaanbaatar",
145 "Seoul" => "Asia/Seoul",
146 "Osaka" => "Asia/Tokyo",
147 "Sapporo" => "Asia/Tokyo",
148 "Tokyo" => "Asia/Tokyo",
149 "Yakutsk" => "Asia/Yakutsk",
150 "Darwin" => "Australia/Darwin",
151 "Adelaide" => "Australia/Adelaide",
152 "Canberra" => "Australia/Melbourne",
153 "Melbourne" => "Australia/Melbourne",
154 "Sydney" => "Australia/Sydney",
155 "Brisbane" => "Australia/Brisbane",
156 "Hobart" => "Australia/Hobart",
157 "Vladivostok" => "Asia/Vladivostok",
158 "Guam" => "Pacific/Guam",
159 "Port Moresby" => "Pacific/Port_Moresby",
160 "Magadan" => "Asia/Magadan",
161 "Solomon Is." => "Asia/Magadan",
162 "New Caledonia" => "Pacific/Noumea",
163 "Fiji" => "Pacific/Fiji",
164 "Kamchatka" => "Asia/Kamchatka",
165 "Marshall Is." => "Pacific/Majuro",
166 "Auckland" => "Pacific/Auckland",
167 "Wellington" => "Pacific/Auckland",
168 "Nuku'alofa" => "Pacific/Tongatapu"
169 }.each { |name, zone| name.freeze; zone.freeze }
170 MAPPING.freeze
171 end
172
173 include Comparable
174 attr_reader :name
175
176 # Create a new TimeZone object with the given name and offset. The
177 # offset is the number of seconds that this time zone is offset from UTC
178 # (GMT). Seconds were chosen as the offset unit because that is the unit that
179 # Ruby uses to represent time zone offsets (see Time#utc_offset).
180 def initialize(name, utc_offset, tzinfo = nil)
181 @name = name
182 @utc_offset = utc_offset
183 @tzinfo = tzinfo
184 end
185
186 def utc_offset
187 @utc_offset ||= tzinfo.current_period.utc_offset
188 end
189
190 # Returns the offset of this time zone as a formatted string, of the
191 # format "+HH:MM".
192 def formatted_offset(colon=true, alternate_utc_string = nil)
193 utc_offset == 0 && alternate_utc_string || utc_offset.to_utc_offset_s(colon)
194 end
195
196 # Compare this time zone to the parameter. The two are comapred first on
197 # their offsets, and then by name.
198 def <=>(zone)
199 result = (utc_offset <=> zone.utc_offset)
200 result = (name <=> zone.name) if result == 0
201 result
202 end
203
204 # Compare #name and TZInfo identifier to a supplied regexp, returning true
205 # if a match is found.
206 def =~(re)
207 return true if name =~ re || MAPPING[name] =~ re
208 end
209
210 # Returns a textual representation of this time zone.
211 def to_s
212 "(GMT#{formatted_offset}) #{name}"
213 end
214
215 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from given values. Example:
216 #
217 # Time.zone = "Hawaii" # => "Hawaii"
218 # Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
219 def local(*args)
220 time = Time.utc_time(*args)
221 ActiveSupport::TimeWithZone.new(nil, self, time)
222 end
223
224 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from number of seconds since the Unix epoch. Example:
225 #
226 # Time.zone = "Hawaii" # => "Hawaii"
227 # Time.utc(2000).to_f # => 946684800.0
228 # Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00
229 def at(secs)
230 utc = Time.at(secs).utc rescue DateTime.civil(1970).since(secs)
231 utc.in_time_zone(self)
232 end
233
234 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from parsed string. Example:
235 #
236 # Time.zone = "Hawaii" # => "Hawaii"
237 # Time.zone.parse('1999-12-31 14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
238 #
239 # If upper components are missing from the string, they are supplied from TimeZone#now:
240 #
241 # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
242 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
243 def parse(str, now=now)
244 date_parts = Date._parse(str)
245 return if date_parts.blank?
246 time = Time.parse(str, now) rescue DateTime.parse(str)
247 if date_parts[:offset].nil?
248 ActiveSupport::TimeWithZone.new(nil, self, time)
249 else
250 time.in_time_zone(self)
251 end
252 end
253
254 # Returns an ActiveSupport::TimeWithZone instance representing the current time
255 # in the time zone represented by +self+. Example:
256 #
257 # Time.zone = 'Hawaii' # => "Hawaii"
258 # Time.zone.now # => Wed, 23 Jan 2008 20:24:27 HST -10:00
259 def now
260 Time.now.utc.in_time_zone(self)
261 end
262
263 # Return the current date in this time zone.
264 def today
265 tzinfo.now.to_date
266 end
267
268 # Adjust the given time to the simultaneous time in the time zone represented by +self+. Returns a
269 # Time.utc() instance -- if you want an ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead.
270 def utc_to_local(time)
271 tzinfo.utc_to_local(time)
272 end
273
274 # Adjust the given time to the simultaneous time in UTC. Returns a Time.utc() instance.
275 def local_to_utc(time, dst=true)
276 tzinfo.local_to_utc(time, dst)
277 end
278
279 # Available so that TimeZone instances respond like TZInfo::Timezone instances
280 def period_for_utc(time)
281 tzinfo.period_for_utc(time)
282 end
283
284 # Available so that TimeZone instances respond like TZInfo::Timezone instances
285 def period_for_local(time, dst=true)
286 tzinfo.period_for_local(time, dst)
287 end
288
289 # TODO: Preload instead of lazy load for thread safety
290 def tzinfo
291 @tzinfo ||= TZInfo::Timezone.get(MAPPING[name])
292 end
293
294 unless const_defined?(:ZONES)
295 ZONES = []
296 ZONES_MAP = {}
297 [[-39_600, "International Date Line West", "Midway Island", "Samoa" ],
298 [-36_000, "Hawaii" ],
299 [-32_400, "Alaska" ],
300 [-28_800, "Pacific Time (US & Canada)", "Tijuana" ],
301 [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "Mazatlan",
302 "Arizona" ],
303 [-21_600, "Central Time (US & Canada)", "Saskatchewan", "Guadalajara",
304 "Mexico City", "Monterrey", "Central America" ],
305 [-18_000, "Eastern Time (US & Canada)", "Indiana (East)", "Bogota",
306 "Lima", "Quito" ],
307 [-16_200, "Caracas" ],
308 [-14_400, "Atlantic Time (Canada)", "La Paz", "Santiago" ],
309 [-12_600, "Newfoundland" ],
310 [-10_800, "Brasilia", "Buenos Aires", "Georgetown", "Greenland" ],
311 [ -7_200, "Mid-Atlantic" ],
312 [ -3_600, "Azores", "Cape Verde Is." ],
313 [ 0, "Dublin", "Edinburgh", "Lisbon", "London", "Casablanca",
314 "Monrovia", "UTC" ],
315 [ 3_600, "Belgrade", "Bratislava", "Budapest", "Ljubljana", "Prague",
316 "Sarajevo", "Skopje", "Warsaw", "Zagreb", "Brussels",
317 "Copenhagen", "Madrid", "Paris", "Amsterdam", "Berlin",
318 "Bern", "Rome", "Stockholm", "Vienna",
319 "West Central Africa" ],
320 [ 7_200, "Bucharest", "Cairo", "Helsinki", "Kyev", "Riga", "Sofia",
321 "Tallinn", "Vilnius", "Athens", "Istanbul", "Minsk",
322 "Jerusalem", "Harare", "Pretoria" ],
323 [ 10_800, "Moscow", "St. Petersburg", "Volgograd", "Kuwait", "Riyadh",
324 "Nairobi", "Baghdad" ],
325 [ 12_600, "Tehran" ],
326 [ 14_400, "Abu Dhabi", "Muscat", "Baku", "Tbilisi", "Yerevan" ],
327 [ 16_200, "Kabul" ],
328 [ 18_000, "Ekaterinburg", "Islamabad", "Karachi", "Tashkent" ],
329 [ 19_800, "Chennai", "Kolkata", "Mumbai", "New Delhi", "Sri Jayawardenepura" ],
330 [ 20_700, "Kathmandu" ],
331 [ 21_600, "Astana", "Dhaka", "Almaty",
332 "Novosibirsk" ],
333 [ 23_400, "Rangoon" ],
334 [ 25_200, "Bangkok", "Hanoi", "Jakarta", "Krasnoyarsk" ],
335 [ 28_800, "Beijing", "Chongqing", "Hong Kong", "Urumqi",
336 "Kuala Lumpur", "Singapore", "Taipei", "Perth", "Irkutsk",
337 "Ulaan Bataar" ],
338 [ 32_400, "Seoul", "Osaka", "Sapporo", "Tokyo", "Yakutsk" ],
339 [ 34_200, "Darwin", "Adelaide" ],
340 [ 36_000, "Canberra", "Melbourne", "Sydney", "Brisbane", "Hobart",
341 "Vladivostok", "Guam", "Port Moresby" ],
342 [ 39_600, "Magadan", "Solomon Is.", "New Caledonia" ],
343 [ 43_200, "Fiji", "Kamchatka", "Marshall Is.", "Auckland",
344 "Wellington" ],
345 [ 46_800, "Nuku'alofa" ]].
346 each do |offset, *places|
347 places.each do |place|
348 place.freeze
349 zone = new(place, offset)
350 ZONES << zone
351 ZONES_MAP[place] = zone
352 end
353 end
354 ZONES.sort!
355 ZONES.freeze
356 ZONES_MAP.freeze
357
358 US_ZONES = ZONES.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
359 US_ZONES.freeze
360 end
361
362 class << self
363 alias_method :create, :new
364
365 # Return a TimeZone instance with the given name, or +nil+ if no
366 # such TimeZone instance exists. (This exists to support the use of
367 # this class with the +composed_of+ macro.)
368 def new(name)
369 self[name]
370 end
371
372 # Return an array of all TimeZone objects. There are multiple
373 # TimeZone objects per time zone, in many cases, to make it easier
374 # for users to find their own time zone.
375 def all
376 ZONES
377 end
378
379 # Locate a specific time zone object. If the argument is a string, it
380 # is interpreted to mean the name of the timezone to locate. If it is a
381 # numeric value it is either the hour offset, or the second offset, of the
382 # timezone to find. (The first one with that offset will be returned.)
383 # Returns +nil+ if no such time zone is known to the system.
384 def [](arg)
385 case arg
386 when String
387 ZONES_MAP[arg]
388 when Numeric, ActiveSupport::Duration
389 arg *= 3600 if arg.abs <= 13
390 all.find { |z| z.utc_offset == arg.to_i }
391 else
392 raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
393 end
394 end
395
396 # A convenience method for returning a collection of TimeZone objects
397 # for time zones in the USA.
398 def us_zones
399 US_ZONES
400 end
401 end
402 end
403 end