Read more

Why you can't use timezone codes like "PST" or "BST" for Time objects

Arne Hartherz
March 28, 2019Software engineer at makandra GmbH

Rails' ActiveSupport::TimeWithZone objects have both a timezone code and offset, e.g. Thu, 28 Mar 2019 16:00:00 CET +01:00. Ruby's stdlib TZInfo also has time zones, but with different identifiers.
Unfortunately, not all timezone codes can be used to parse strings or to move time objects into another time zone.

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more Show archive.org snapshot

Some timezone codes like CET are supported by ActiveSupport extensions like String#in_time_zone, while many codes will actually not work:

>> '2019-03-01 12:00'.in_time_zone('PST')
ArgumentError (Invalid Timezone: PST)

Here is why and what to do about it.
tl;dr: Use ActiveSupport's zone identifiers and convert them to zone codes where necessary (snippet at the end).

The world is a big place

There are many countries in the world, and while some time zones are shared by multiple countries, it does not necessarily have to be that way forever.

Also, some use the same abbreviation for their time zones. For example, "BST" could mean

  • British Summer Time
  • Bangladesh Standard Time
  • Bougainville Standard Time

while "PST" could be

  • Pacific Standard Time (Side note: If you mean this, you could use "PST8PDT")
  • Pitcairn Standard Time

It's actually really good that you can not use identifiers like "BST" because chances are you'll end up in the wrong time zone.

This is why Rails actually uses a long list of time zone names with have names like "London" or "Pacific Time (US & Canada)". They are basically from the IANA Time Zone Database Show archive.org snapshot that TZInfo uses (like "Europe/London"), but simplified.

Why DateTime.strptime is not what you want

You may eventually encounter DateTime.strptime. It's a "better" Time.parse where you can actually define the date format to parse, e.g.

>> DateTime.strptime('2019-03-01 12:00 PST', '%F %R %Z')
=> Fri, 01 Mar 2019 12:00:00 -0800

However, this will fail you in several ways:

  • Rails' DST checks are disabled, so you might parse a DST time with a non-DST code and end up with an object that uses an incorrect time zone and offset.
  • If you use Rails' time_zone_select, you will be juggling zone names like "Berlin" which are actually unsupported.
  • Unsupported zone identifiers are silently discarded and you'll end up with UTC times.
>> DateTime.strptime('2019-03-01 12:00 Berlin', '%F %R %Z')
=> Fri, 01 Mar 2019 12:00:00 +0000

This should not be UTC. And no, Europe/Berlin won't work either. You could pass +01:00 but would still have no DST checks from Rails as described above.

Why Time.zone_offset is not what you want

If you are only interested in a time zone's offset, you might consider Time.zone_offset and it might work sometimes:

>> Time.zone_offset('PST')
=> -28800

Ruby's Time actually keeps a list of custom identifiers Show archive.org snapshot .

Other identifiers will not work and simply return nil.

>> Time.zone_offset('BST')
=> nil
>> Time.zone_offset('Berlin')
=> nil

I'm confident there are other Time methods with similar pitfalls.

ActiveSupport's list of time zones

As mentioned earlier, ActiveSupport actually comes with a long list of time zones that should work well for you. Rails even has a time_zone_select that makes selecting a time zone okay-ish for users.

However, identifiers are Strings like "Berlin", "Pacific Time (US & Canada)" which can be confusing. If your application uses those strings to express time zones, "16:00 Berlin" can be misleading when combined with a location like "Munich".

So you could just read TimeWithZone#zone, right?

>> '2019-03-28 16:00'.in_time_zone('Berlin').zone
=> "CET"

You can, except when you can not:

>> '2019-03-28 16:00'.in_time_zone('Almaty').zone
=> "+06"

Not every time zone has a pretty abbreviation. In such cases, you'll end up with a semi-broken offset identifier that humans won't understand.

What to do?

It's not as bad as it seems.

  • Use ActiveSupport's time zones. It's the best you can get in a Rails application. Don't bother using TZInfo::Timezone.all unless you want to do all the heavy lifting yourself.
    • If your Rails app has to be fully aware of time zones, enable them. Mind that there might be caveats.
    • If you only display relative timestamps (e.g. "one hour ago"), you can get away with not enabling time zones for your application. This is actually good, because it's simpler. To only convert strings into time objects with zone information, use String#in_time_zone from ActiveSupport like above.
  • To render timezone codes when available, but avoid ugly Strings like "+06", do something like this:
def human_timezone(time_string, timezone)
  time = time_string.in_time_zone(timezone)

  if time.zone.match?(/^\w/)
    time.zone
  else
    time.formatted_offset
  end
end

Examples:

>> human_timezone('2019-03-28 16:00', 'Pacific Time (US & Canada)')
=> "PDT"
>> human_timezone('2019-03-28 16:00', 'Berlin')
=> "CET"
>> human_timezone('2019-05-01 16:00', 'Almaty')
=> "+06:00"
Arne Hartherz
March 28, 2019Software engineer at makandra GmbH
Posted by Arne Hartherz to makandra dev (2019-03-28 17:27)