Why you can't use timezone codes like "PST" or "BST" for Time objects
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 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
Time actually keeps a list of custom identifiers.
Other identifiers will not work and simply return
>> 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
"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
>> '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.allunless 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_zonefrom 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
>> 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"