timezone.rb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. #--
  2. # Copyright (c) 2005-2010 Philip Ross
  3. #
  4. # Permission is hereby granted, free of charge, to any person obtaining a copy
  5. # of this software and associated documentation files (the "Software"), to deal
  6. # in the Software without restriction, including without limitation the rights
  7. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. # copies of the Software, and to permit persons to whom the Software is
  9. # furnished to do so, subject to the following conditions:
  10. #
  11. # The above copyright notice and this permission notice shall be included in all
  12. # copies or substantial portions of the Software.
  13. #
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. # THE SOFTWARE.
  21. #++
  22. require 'date'
  23. module TZInfo
  24. # Indicate a specified time in a local timezone has more than one
  25. # possible time in UTC. This happens when switching from daylight savings time
  26. # to normal time where the clocks are rolled back. Thrown by period_for_local
  27. # and local_to_utc when using an ambiguous time and not specifying any
  28. # means to resolve the ambiguity.
  29. class AmbiguousTime < StandardError
  30. end
  31. # Thrown to indicate that no TimezonePeriod matching a given time could be found.
  32. class PeriodNotFound < StandardError
  33. end
  34. # Thrown by Timezone#get if the identifier given is not valid.
  35. class InvalidTimezoneIdentifier < StandardError
  36. end
  37. # Thrown if an attempt is made to use a timezone created with Timezone.new(nil).
  38. class UnknownTimezone < StandardError
  39. end
  40. # Timezone is the base class of all timezones. It provides a factory method
  41. # get to access timezones by identifier. Once a specific Timezone has been
  42. # retrieved, DateTimes, Times and timestamps can be converted between the UTC
  43. # and the local time for the zone. For example:
  44. #
  45. # tz = TZInfo::Timezone.get('America/New_York')
  46. # puts tz.utc_to_local(DateTime.new(2005,8,29,15,35,0)).to_s
  47. # puts tz.local_to_utc(Time.utc(2005,8,29,11,35,0)).to_s
  48. # puts tz.utc_to_local(1125315300).to_s
  49. #
  50. # Each time conversion method returns an object of the same type it was
  51. # passed.
  52. #
  53. # The timezone information all comes from the tz database
  54. # (see http://www.twinsun.com/tz/tz-link.htm)
  55. class Timezone
  56. include Comparable
  57. # Cache of loaded zones by identifier to avoid using require if a zone
  58. # has already been loaded.
  59. @@loaded_zones = {}
  60. # Whether the timezones index has been loaded yet.
  61. @@index_loaded = false
  62. # Default value of the dst parameter of the local_to_utc and
  63. # period_for_local methods.
  64. @@default_dst = nil
  65. # Sets the default value of the optional dst parameter of the
  66. # local_to_utc and period_for_local methods. Can be set to nil, true or
  67. # false.
  68. #
  69. # The value of default_dst defaults to nil if unset.
  70. def self.default_dst=(value)
  71. @@default_dst = value.nil? ? nil : !!value
  72. end
  73. # Gets the default value of the optional dst parameter of the
  74. # local_to_utc and period_for_local methods. Can be set to nil, true or
  75. # false.
  76. def self.default_dst
  77. @@default_dst
  78. end
  79. # Returns a timezone by its identifier (e.g. "Europe/London",
  80. # "America/Chicago" or "UTC").
  81. #
  82. # Raises InvalidTimezoneIdentifier if the timezone couldn't be found.
  83. def self.get(identifier)
  84. instance = @@loaded_zones[identifier]
  85. unless instance
  86. raise InvalidTimezoneIdentifier, 'Invalid identifier' if identifier !~ /^[A-Za-z0-9\+\-_]+(\/[A-Za-z0-9\+\-_]+)*$/
  87. identifier = identifier.gsub(/-/, '__m__').gsub(/\+/, '__p__')
  88. begin
  89. # Use a temporary variable to avoid an rdoc warning
  90. file = "tzinfo/definitions/#{identifier}".untaint
  91. require file
  92. m = Definitions
  93. identifier.split(/\//).each {|part|
  94. m = m.const_get(part)
  95. }
  96. info = m.get
  97. # Could make Timezone subclasses register an interest in an info
  98. # type. Since there are currently only two however, there isn't
  99. # much point.
  100. if info.kind_of?(DataTimezoneInfo)
  101. instance = DataTimezone.new(info)
  102. elsif info.kind_of?(LinkedTimezoneInfo)
  103. instance = LinkedTimezone.new(info)
  104. else
  105. raise InvalidTimezoneIdentifier, "No handler for info type #{info.class}"
  106. end
  107. @@loaded_zones[instance.identifier] = instance
  108. rescue LoadError, NameError => e
  109. raise InvalidTimezoneIdentifier, e.message
  110. end
  111. end
  112. instance
  113. end
  114. # Returns a proxy for the Timezone with the given identifier. The proxy
  115. # will cause the real timezone to be loaded when an attempt is made to
  116. # find a period or convert a time. get_proxy will not validate the
  117. # identifier. If an invalid identifier is specified, no exception will be
  118. # raised until the proxy is used.
  119. def self.get_proxy(identifier)
  120. TimezoneProxy.new(identifier)
  121. end
  122. # If identifier is nil calls super(), otherwise calls get. An identfier
  123. # should always be passed in when called externally.
  124. def self.new(identifier = nil)
  125. if identifier
  126. get(identifier)
  127. else
  128. super()
  129. end
  130. end
  131. # Returns an array containing all the available Timezones.
  132. #
  133. # Returns TimezoneProxy objects to avoid the overhead of loading Timezone
  134. # definitions until a conversion is actually required.
  135. def self.all
  136. get_proxies(all_identifiers)
  137. end
  138. # Returns an array containing the identifiers of all the available
  139. # Timezones.
  140. def self.all_identifiers
  141. load_index
  142. Indexes::Timezones.timezones
  143. end
  144. # Returns an array containing all the available Timezones that are based
  145. # on data (are not links to other Timezones).
  146. #
  147. # Returns TimezoneProxy objects to avoid the overhead of loading Timezone
  148. # definitions until a conversion is actually required.
  149. def self.all_data_zones
  150. get_proxies(all_data_zone_identifiers)
  151. end
  152. # Returns an array containing the identifiers of all the available
  153. # Timezones that are based on data (are not links to other Timezones)..
  154. def self.all_data_zone_identifiers
  155. load_index
  156. Indexes::Timezones.data_timezones
  157. end
  158. # Returns an array containing all the available Timezones that are links
  159. # to other Timezones.
  160. #
  161. # Returns TimezoneProxy objects to avoid the overhead of loading Timezone
  162. # definitions until a conversion is actually required.
  163. def self.all_linked_zones
  164. get_proxies(all_linked_zone_identifiers)
  165. end
  166. # Returns an array containing the identifiers of all the available
  167. # Timezones that are links to other Timezones.
  168. def self.all_linked_zone_identifiers
  169. load_index
  170. Indexes::Timezones.linked_timezones
  171. end
  172. # Returns all the Timezones defined for all Countries. This is not the
  173. # complete set of Timezones as some are not country specific (e.g.
  174. # 'Etc/GMT').
  175. #
  176. # Returns TimezoneProxy objects to avoid the overhead of loading Timezone
  177. # definitions until a conversion is actually required.
  178. def self.all_country_zones
  179. Country.all_codes.inject([]) {|zones,country|
  180. zones += Country.get(country).zones
  181. }
  182. end
  183. # Returns all the zone identifiers defined for all Countries. This is not the
  184. # complete set of zone identifiers as some are not country specific (e.g.
  185. # 'Etc/GMT'). You can obtain a Timezone instance for a given identifier
  186. # with the get method.
  187. def self.all_country_zone_identifiers
  188. Country.all_codes.inject([]) {|zones,country|
  189. zones += Country.get(country).zone_identifiers
  190. }
  191. end
  192. # Returns all US Timezone instances. A shortcut for
  193. # TZInfo::Country.get('US').zones.
  194. #
  195. # Returns TimezoneProxy objects to avoid the overhead of loading Timezone
  196. # definitions until a conversion is actually required.
  197. def self.us_zones
  198. Country.get('US').zones
  199. end
  200. # Returns all US zone identifiers. A shortcut for
  201. # TZInfo::Country.get('US').zone_identifiers.
  202. def self.us_zone_identifiers
  203. Country.get('US').zone_identifiers
  204. end
  205. # The identifier of the timezone, e.g. "Europe/Paris".
  206. def identifier
  207. raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
  208. end
  209. # An alias for identifier.
  210. def name
  211. # Don't use alias, as identifier gets overridden.
  212. identifier
  213. end
  214. # Returns a friendlier version of the identifier.
  215. def to_s
  216. friendly_identifier
  217. end
  218. # Returns internal object state as a programmer-readable string.
  219. def inspect
  220. "#<#{self.class}: #{identifier}>"
  221. end
  222. # Returns a friendlier version of the identifier. Set skip_first_part to
  223. # omit the first part of the identifier (typically a region name) where
  224. # there is more than one part.
  225. #
  226. # For example:
  227. #
  228. # Timezone.get('Europe/Paris').friendly_identifier(false) #=> "Europe - Paris"
  229. # Timezone.get('Europe/Paris').friendly_identifier(true) #=> "Paris"
  230. # Timezone.get('America/Indiana/Knox').friendly_identifier(false) #=> "America - Knox, Indiana"
  231. # Timezone.get('America/Indiana/Knox').friendly_identifier(true) #=> "Knox, Indiana"
  232. def friendly_identifier(skip_first_part = false)
  233. parts = identifier.split('/')
  234. if parts.empty?
  235. # shouldn't happen
  236. identifier
  237. elsif parts.length == 1
  238. parts[0]
  239. else
  240. if skip_first_part
  241. result = ''
  242. else
  243. result = parts[0] + ' - '
  244. end
  245. parts[1, parts.length - 1].reverse_each {|part|
  246. part.gsub!(/_/, ' ')
  247. if part.index(/[a-z]/)
  248. # Missing a space if a lower case followed by an upper case and the
  249. # name isn't McXxxx.
  250. part.gsub!(/([^M][a-z])([A-Z])/, '\1 \2')
  251. part.gsub!(/([M][a-bd-z])([A-Z])/, '\1 \2')
  252. # Missing an apostrophe if two consecutive upper case characters.
  253. part.gsub!(/([A-Z])([A-Z])/, '\1\'\2')
  254. end
  255. result << part
  256. result << ', '
  257. }
  258. result.slice!(result.length - 2, 2)
  259. result
  260. end
  261. end
  262. # Returns the TimezonePeriod for the given UTC time. utc can either be
  263. # a DateTime, Time or integer timestamp (Time.to_i). Any timezone
  264. # information in utc is ignored (it is treated as a UTC time).
  265. def period_for_utc(utc)
  266. raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
  267. end
  268. # Returns the set of TimezonePeriod instances that are valid for the given
  269. # local time as an array. If you just want a single period, use
  270. # period_for_local instead and specify how ambiguities should be resolved.
  271. # Returns an empty array if no periods are found for the given time.
  272. def periods_for_local(local)
  273. raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
  274. end
  275. # Returns the TimezonePeriod for the given local time. local can either be
  276. # a DateTime, Time or integer timestamp (Time.to_i). Any timezone
  277. # information in local is ignored (it is treated as a time in the current
  278. # timezone).
  279. #
  280. # Warning: There are local times that have no equivalent UTC times (e.g.
  281. # in the transition from standard time to daylight savings time). There are
  282. # also local times that have more than one UTC equivalent (e.g. in the
  283. # transition from daylight savings time to standard time).
  284. #
  285. # In the first case (no equivalent UTC time), a PeriodNotFound exception
  286. # will be raised.
  287. #
  288. # In the second case (more than one equivalent UTC time), an AmbiguousTime
  289. # exception will be raised unless the optional dst parameter or block
  290. # handles the ambiguity.
  291. #
  292. # If the ambiguity is due to a transition from daylight savings time to
  293. # standard time, the dst parameter can be used to select whether the
  294. # daylight savings time or local time is used. For example,
  295. #
  296. # Timezone.get('America/New_York').period_for_local(DateTime.new(2004,10,31,1,30,0))
  297. #
  298. # would raise an AmbiguousTime exception.
  299. #
  300. # Specifying dst=true would the daylight savings period from April to
  301. # October 2004. Specifying dst=false would return the standard period
  302. # from October 2004 to April 2005.
  303. #
  304. # If the dst parameter does not resolve the ambiguity, and a block is
  305. # specified, it is called. The block must take a single parameter - an
  306. # array of the periods that need to be resolved. The block can select and
  307. # return a single period or return nil or an empty array
  308. # to cause an AmbiguousTime exception to be raised.
  309. #
  310. # The default value of the dst parameter can be specified by setting
  311. # Timezone.default_dst. If default_dst is not set, or is set to nil, then
  312. # an AmbiguousTime exception will be raised in ambiguous situations unless
  313. # a block is given to resolve the ambiguity.
  314. def period_for_local(local, dst = Timezone.default_dst)
  315. results = periods_for_local(local)
  316. if results.empty?
  317. raise PeriodNotFound
  318. elsif results.size < 2
  319. results.first
  320. else
  321. # ambiguous result try to resolve
  322. if !dst.nil?
  323. matches = results.find_all {|period| period.dst? == dst}
  324. results = matches if !matches.empty?
  325. end
  326. if results.size < 2
  327. results.first
  328. else
  329. # still ambiguous, try the block
  330. if block_given?
  331. results = yield results
  332. end
  333. if results.is_a?(TimezonePeriod)
  334. results
  335. elsif results && results.size == 1
  336. results.first
  337. else
  338. raise AmbiguousTime, "#{local} is an ambiguous local time."
  339. end
  340. end
  341. end
  342. end
  343. # Converts a time in UTC to the local timezone. utc can either be
  344. # a DateTime, Time or timestamp (Time.to_i). The returned time has the same
  345. # type as utc. Any timezone information in utc is ignored (it is treated as
  346. # a UTC time).
  347. def utc_to_local(utc)
  348. TimeOrDateTime.wrap(utc) {|wrapped|
  349. period_for_utc(wrapped).to_local(wrapped)
  350. }
  351. end
  352. # Converts a time in the local timezone to UTC. local can either be
  353. # a DateTime, Time or timestamp (Time.to_i). The returned time has the same
  354. # type as local. Any timezone information in local is ignored (it is treated
  355. # as a local time).
  356. #
  357. # Warning: There are local times that have no equivalent UTC times (e.g.
  358. # in the transition from standard time to daylight savings time). There are
  359. # also local times that have more than one UTC equivalent (e.g. in the
  360. # transition from daylight savings time to standard time).
  361. #
  362. # In the first case (no equivalent UTC time), a PeriodNotFound exception
  363. # will be raised.
  364. #
  365. # In the second case (more than one equivalent UTC time), an AmbiguousTime
  366. # exception will be raised unless the optional dst parameter or block
  367. # handles the ambiguity.
  368. #
  369. # If the ambiguity is due to a transition from daylight savings time to
  370. # standard time, the dst parameter can be used to select whether the
  371. # daylight savings time or local time is used. For example,
  372. #
  373. # Timezone.get('America/New_York').local_to_utc(DateTime.new(2004,10,31,1,30,0))
  374. #
  375. # would raise an AmbiguousTime exception.
  376. #
  377. # Specifying dst=true would return 2004-10-31 5:30:00. Specifying dst=false
  378. # would return 2004-10-31 6:30:00.
  379. #
  380. # If the dst parameter does not resolve the ambiguity, and a block is
  381. # specified, it is called. The block must take a single parameter - an
  382. # array of the periods that need to be resolved. The block can return a
  383. # single period to use to convert the time or return nil or an empty array
  384. # to cause an AmbiguousTime exception to be raised.
  385. #
  386. # The default value of the dst parameter can be specified by setting
  387. # Timezone.default_dst. If default_dst is not set, or is set to nil, then
  388. # an AmbiguousTime exception will be raised in ambiguous situations unless
  389. # a block is given to resolve the ambiguity.
  390. def local_to_utc(local, dst = Timezone.default_dst)
  391. TimeOrDateTime.wrap(local) {|wrapped|
  392. if block_given?
  393. period = period_for_local(wrapped, dst) {|periods| yield periods }
  394. else
  395. period = period_for_local(wrapped, dst)
  396. end
  397. period.to_utc(wrapped)
  398. }
  399. end
  400. # Returns the current time in the timezone as a Time.
  401. def now
  402. utc_to_local(Time.now.utc)
  403. end
  404. # Returns the TimezonePeriod for the current time.
  405. def current_period
  406. period_for_utc(Time.now.utc)
  407. end
  408. # Returns the current Time and TimezonePeriod as an array. The first element
  409. # is the time, the second element is the period.
  410. def current_period_and_time
  411. utc = Time.now.utc
  412. period = period_for_utc(utc)
  413. [period.to_local(utc), period]
  414. end
  415. alias :current_time_and_period :current_period_and_time
  416. # Converts a time in UTC to local time and returns it as a string
  417. # according to the given format. The formatting is identical to
  418. # Time.strftime and DateTime.strftime, except %Z is replaced with the
  419. # timezone abbreviation for the specified time (for example, EST or EDT).
  420. def strftime(format, utc = Time.now.utc)
  421. period = period_for_utc(utc)
  422. local = period.to_local(utc)
  423. local = Time.at(local).utc unless local.kind_of?(Time) || local.kind_of?(DateTime)
  424. abbreviation = period.abbreviation.to_s.gsub(/%/, '%%')
  425. format = format.gsub(/(.?)%Z/) do
  426. if $1 == '%'
  427. # return %%Z so the real strftime treats it as a literal %Z too
  428. '%%Z'
  429. else
  430. "#$1#{abbreviation}"
  431. end
  432. end
  433. local.strftime(format)
  434. end
  435. # Compares two Timezones based on their identifier. Returns -1 if tz is less
  436. # than self, 0 if tz is equal to self and +1 if tz is greater than self.
  437. def <=>(tz)
  438. identifier <=> tz.identifier
  439. end
  440. # Returns true if and only if the identifier of tz is equal to the
  441. # identifier of this Timezone.
  442. def eql?(tz)
  443. self == tz
  444. end
  445. # Returns a hash of this Timezone.
  446. def hash
  447. identifier.hash
  448. end
  449. # Dumps this Timezone for marshalling.
  450. def _dump(limit)
  451. identifier
  452. end
  453. # Loads a marshalled Timezone.
  454. def self._load(data)
  455. Timezone.get(data)
  456. end
  457. private
  458. # Loads in the index of timezones if it hasn't already been loaded.
  459. def self.load_index
  460. unless @@index_loaded
  461. require 'tzinfo/indexes/timezones'
  462. @@index_loaded = true
  463. end
  464. end
  465. # Returns an array of proxies corresponding to the given array of
  466. # identifiers.
  467. def self.get_proxies(identifiers)
  468. identifiers.collect {|identifier| get_proxy(identifier)}
  469. end
  470. end
  471. end