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 attr_reader :tzinfo
176
177 # Create a new TimeZone object with the given name and offset. The
178 # offset is the number of seconds that this time zone is offset from UTC
179 # (GMT). Seconds were chosen as the offset unit because that is the unit that
180 # Ruby uses to represent time zone offsets (see Time#utc_offset).
181 def initialize(name, utc_offset = nil, tzinfo = nil)
182 @name = name
183 @utc_offset = utc_offset
184 @tzinfo = tzinfo || TimeZone.find_tzinfo(name)
185 @current_period = nil
186 end
187
188 def utc_offset
189 if @utc_offset
190 @utc_offset
191 else
192 @current_period ||= tzinfo.current_period
193 @current_period.utc_offset
194 end
195 end
196
197 # Returns the offset of this time zone as a formatted string, of the
198 # format "+HH:MM".
199 def formatted_offset(colon=true, alternate_utc_string = nil)
200 utc_offset == 0 && alternate_utc_string || utc_offset.to_utc_offset_s(colon)
201 end
202
203 # Compare this time zone to the parameter. The two are comapred first on
204 # their offsets, and then by name.
205 def <=>(zone)
206 result = (utc_offset <=> zone.utc_offset)
207 result = (name <=> zone.name) if result == 0
208 result
209 end
210
211 # Compare #name and TZInfo identifier to a supplied regexp, returning true
212 # if a match is found.
213 def =~(re)
214 return true if name =~ re || MAPPING[name] =~ re
215 end
216
217 # Returns a textual representation of this time zone.
218 def to_s
219 "(GMT#{formatted_offset}) #{name}"
220 end
221
222 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from given values. Example:
223 #
224 # Time.zone = "Hawaii" # => "Hawaii"
225 # Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
226 def local(*args)
227 time = Time.utc_time(*args)
228 ActiveSupport::TimeWithZone.new(nil, self, time)
229 end
230
231 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from number of seconds since the Unix epoch. Example:
232 #
233 # Time.zone = "Hawaii" # => "Hawaii"
234 # Time.utc(2000).to_f # => 946684800.0
235 # Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00
236 def at(secs)
237 utc = Time.at(secs).utc rescue DateTime.civil(1970).since(secs)
238 utc.in_time_zone(self)
239 end
240
241 # Method for creating new ActiveSupport::TimeWithZone instance in time zone of +self+ from parsed string. Example:
242 #
243 # Time.zone = "Hawaii" # => "Hawaii"
244 # Time.zone.parse('1999-12-31 14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
245 #
246 # If upper components are missing from the string, they are supplied from TimeZone#now:
247 #
248 # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
249 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
250 def parse(str, now=now)
251 date_parts = Date._parse(str)
252 return if date_parts.blank?
253 time = Time.parse(str, now) rescue DateTime.parse(str)
254 if date_parts[:offset].nil?
255 ActiveSupport::TimeWithZone.new(nil, self, time)
256 else
257 time.in_time_zone(self)
258 end
259 end
260
261 # Returns an ActiveSupport::TimeWithZone instance representing the current time
262 # in the time zone represented by +self+. Example:
263 #
264 # Time.zone = 'Hawaii' # => "Hawaii"
265 # Time.zone.now # => Wed, 23 Jan 2008 20:24:27 HST -10:00
266 def now
267 Time.now.utc.in_time_zone(self)
268 end
269
270 # Return the current date in this time zone.
271 def today
272 tzinfo.now.to_date
273 end
274
275 # Adjust the given time to the simultaneous time in the time zone represented by +self+. Returns a
276 # Time.utc() instance -- if you want an ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead.
277 def utc_to_local(time)
278 tzinfo.utc_to_local(time)
279 end
280
281 # Adjust the given time to the simultaneous time in UTC. Returns a Time.utc() instance.
282 def local_to_utc(time, dst=true)
283 tzinfo.local_to_utc(time, dst)
284 end
285
286 # Available so that TimeZone instances respond like TZInfo::Timezone instances
287 def period_for_utc(time)
288 tzinfo.period_for_utc(time)
289 end
290
291 # Available so that TimeZone instances respond like TZInfo::Timezone instances
292 def period_for_local(time, dst=true)
293 tzinfo.period_for_local(time, dst)
294 end
295
296 # TODO: Preload instead of lazy load for thread safety
297 def self.find_tzinfo(name)
298 require 'tzinfo' unless defined?(TZInfo)
299 ::TZInfo::Timezone.get(MAPPING[name] || name)
300 rescue TZInfo::InvalidTimezoneIdentifier
301 nil
302 end
303
304 class << self
305 alias_method :create, :new
306
307 # Return a TimeZone instance with the given name, or +nil+ if no
308 # such TimeZone instance exists. (This exists to support the use of
309 # this class with the +composed_of+ macro.)
310 def new(name)
311 self[name]
312 end
313
314 # Return an array of all TimeZone objects. There are multiple
315 # TimeZone objects per time zone, in many cases, to make it easier
316 # for users to find their own time zone.
317 def all
318 @zones ||= zones_map.values.sort
319 end
320
321 def zones_map
322 unless defined?(@zones_map)
323 @zones_map = {}
324 [[-39_600, "International Date Line West", "Midway Island", "Samoa" ],
325 [-36_000, "Hawaii" ],
326 [-32_400, "Alaska" ],
327 [-28_800, "Pacific Time (US & Canada)", "Tijuana" ],
328 [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "Mazatlan",
329 "Arizona" ],
330 [-21_600, "Central Time (US & Canada)", "Saskatchewan", "Guadalajara",
331 "Mexico City", "Monterrey", "Central America" ],
332 [-18_000, "Eastern Time (US & Canada)", "Indiana (East)", "Bogota",
333 "Lima", "Quito" ],
334 [-16_200, "Caracas" ],
335 [-14_400, "Atlantic Time (Canada)", "La Paz", "Santiago" ],
336 [-12_600, "Newfoundland" ],
337 [-10_800, "Brasilia", "Buenos Aires", "Georgetown", "Greenland" ],
338 [ -7_200, "Mid-Atlantic" ],
339 [ -3_600, "Azores", "Cape Verde Is." ],
340 [ 0, "Dublin", "Edinburgh", "Lisbon", "London", "Casablanca",
341 "Monrovia", "UTC" ],
342 [ 3_600, "Belgrade", "Bratislava", "Budapest", "Ljubljana", "Prague",
343 "Sarajevo", "Skopje", "Warsaw", "Zagreb", "Brussels",
344 "Copenhagen", "Madrid", "Paris", "Amsterdam", "Berlin",
345 "Bern", "Rome", "Stockholm", "Vienna",
346 "West Central Africa" ],
347 [ 7_200, "Bucharest", "Cairo", "Helsinki", "Kyev", "Riga", "Sofia",
348 "Tallinn", "Vilnius", "Athens", "Istanbul", "Minsk",
349 "Jerusalem", "Harare", "Pretoria" ],
350 [ 10_800, "Moscow", "St. Petersburg", "Volgograd", "Kuwait", "Riyadh",
351 "Nairobi", "Baghdad" ],
352 [ 12_600, "Tehran" ],
353 [ 14_400, "Abu Dhabi", "Muscat", "Baku", "Tbilisi", "Yerevan" ],
354 [ 16_200, "Kabul" ],
355 [ 18_000, "Ekaterinburg", "Islamabad", "Karachi", "Tashkent" ],
356 [ 19_800, "Chennai", "Kolkata", "Mumbai", "New Delhi", "Sri Jayawardenepura" ],
357 [ 20_700, "Kathmandu" ],
358 [ 21_600, "Astana", "Dhaka", "Almaty",
359 "Novosibirsk" ],
360 [ 23_400, "Rangoon" ],
361 [ 25_200, "Bangkok", "Hanoi", "Jakarta", "Krasnoyarsk" ],
362 [ 28_800, "Beijing", "Chongqing", "Hong Kong", "Urumqi",
363 "Kuala Lumpur", "Singapore", "Taipei", "Perth", "Irkutsk",
364 "Ulaan Bataar" ],
365 [ 32_400, "Seoul", "Osaka", "Sapporo", "Tokyo", "Yakutsk" ],
366 [ 34_200, "Darwin", "Adelaide" ],
367 [ 36_000, "Canberra", "Melbourne", "Sydney", "Brisbane", "Hobart",
368 "Vladivostok", "Guam", "Port Moresby" ],
369 [ 39_600, "Magadan", "Solomon Is.", "New Caledonia" ],
370 [ 43_200, "Fiji", "Kamchatka", "Marshall Is.", "Auckland",
371 "Wellington" ],
372 [ 46_800, "Nuku'alofa" ]].
373 each do |offset, *places|
374 places.each do |place|
375 @zones_map[place] = create(place, offset)
376 end
377 end
378 end
379 @zones_map
380 end
381
382 # Locate a specific time zone object. If the argument is a string, it
383 # is interpreted to mean the name of the timezone to locate. If it is a
384 # numeric value it is either the hour offset, or the second offset, of the
385 # timezone to find. (The first one with that offset will be returned.)
386 # Returns +nil+ if no such time zone is known to the system.
387 def [](arg)
388 case arg
389 when String
390 if tz = zones_map[arg]
391 tz
392 elsif tz = lookup(arg)
393 zones_map[arg] = tz
394 end
395 when Numeric, ActiveSupport::Duration
396 arg *= 3600 if arg.abs <= 13
397 all.find { |z| z.utc_offset == arg.to_i }
398 else
399 raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
400 end
401 end
402
403 # A convenience method for returning a collection of TimeZone objects
404 # for time zones in the USA.
405 def us_zones
406 @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
407 end
408
409 private
410
411 def lookup(name)
412 (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze)
413 end
414 end
415 end
416 end