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.
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"