Ruby TZInfo, and ActiveSupport::TimeZone, fail to identify most timezone abbreviations

962 views Asked by At

In Ruby, with ActiveSupport and TZInfo present, I’m trying to parse arbitrary time strings that can include a timezone identifier.

Handling the timezone when given as an offset (e.g., '-08:00') is no problem. And both styles of long names seem to work (e.g., 'America/Vancouver', 'Pacific Time (US & Canada)'), but the common abbreviations, and their daylight-savings alternates, mostly fail—with a few exceptions.

Looking at the List of time zone abbreviations on Wikipedia, there are 190 distinct abbreviations. When I test each one of them with TZInfo::Timezone.get(abbrev) (where abbrev is a variable holding a given abbreviation string, such as 'MST'), only 9 are recognized: CET, EET, EST, GMT, HST, MET, MST, UTC, and WET.

For example:

> TZInfo::Timezone.get('MST')
 => #<TZInfo::DataTimezone: MST>
> TZInfo::Timezone.get('PST')
TZInfo::InvalidTimezoneIdentifier

So: Is there a method that will convert most, or all, of the common 3-5 character timezone abbreviations to a TZInfo or ActiveSupport timezone?

Or will I have to write my own conversion helper, trying to keep the conversion table up to date as timezone policies change around the world?

(I do recognize that the abbreviations are not 100% reliable or authoritative—especially as there are a few cases where the same abbreviation refers to multiple timezones with different offsets—but I’m still needing to make a “best guess” rather than none.)

2

There are 2 answers

0
D. SM On

tzinfo describes where the data it uses comes from:

  • A zoneinfo directory containing timezone definition files. These files are generated from the IANA Time Zone Database using the zic utility. Most Unix-like systems include a zoneinfo directory.

  • The TZInfo::Data library (the tzinfo-data gem). TZInfo::Data contains a set of Ruby modules that are also generated from the IANA Time Zone Database.

The timezone definition files are often located in /usr/share/zoneinfo. On my machine there is nothing in this directory about, for example, PMDT.

The tzinfo-data is here, doesn't have PMDT either.

If you locate a suitable set of timezone info definition files that contains the zones you want to query, tzinfo should in theory be able to use them.

0
Phil Ross On

TZInfo and ActiveSupport don't support looking time zones up by their abbreviation. Those abbreviations that are available with TZInfo::Timezone.get are actually identifiers for legacy time zones in the Time Zone Database.

You can use TZInfo to build your own mapping from abbreviation to time zone identifiers though. For example:

# Consider abbreviations used in the current year.
from = Time.utc(Time.now.utc.year)
to = Time.utc(from.year + 1)

# Build an array of [abbreviation, identifier] pairs.
abbrev_identifiers = TZInfo::Timezone.all_data_zones.flat_map do |tz|
  abbrevs = tz.offsets_up_to(to, from).map {|o| o.abbreviation.to_s }.uniq
  abbrevs.map {|a| [a, tz.identifier] }
end

# Create a Hash using abbreviation as the key and an array of identifiers as the value.
lookup = abbrev_identifiers.each.with_object(Hash.new {|h,k| h[k] = [] }) {|(a, i), h| h[a] << i }

You can now use lookup to get the time zone identifiers (suitable for use with TZInfo::Timezone.get). For example with MST:

p lookup['MST']
#=> ["America/Boise", "America/Cambridge_Bay", "America/Chihuahua", "America/Creston", "America/Dawson", "America/Dawson_Creek", "America/Denver", "America/Edmonton", "America/Fort_Nelson", "America/Hermosillo", "America/Inuvik", "America/Mazatlan", "America/Ojinaga", "America/Phoenix", "America/Whitehorse", "America/Yellowknife", "MST", "MST7MDT"]

Each of those time zones will use different rules at some point since 1970 (with Time Zone Database releases from the last few years at least). If you're only interested in handling times within a smaller window, then you could filter the lookup down to just time zones that have different rules within that window:

current_year = lookup.map.with_object(Hash.new) do |(a, z), h|
  h[a] = z.uniq {|i| TZInfo::Timezone.get(i).transitions_up_to(to, from) }
end

p current_year['MST']
#=> ["America/Boise", "America/Chihuahua", "America/Creston", "America/Dawson"]

Depending on your application, you might want to pre-compute and store the lookup. Iterating through the time zones will load each into memory for the lifetime of the process.

Note that the abbreviations used in the Time Zone Database and returned by TZInfo are different to those on the List of time zone abbreviations Wikipedia page you linked to. In a lot of cases, there are no standard abbreviations and the Time Zone Database and Wikipedia authors will be using different sources and methodologies.