Search code examples
rubytimezonedstactivesupport

How to get time_zone_options_for_select with DST offsets?


The ActionView::Helpers::FormOptionsHelper provides the time_zone_options_for_select to get a list of options for a select control that includes all of the timezones with their UTC offset. The problem I'm having is how to get it to display the correct offset for when daylight savings time is in effect?

For instance U.S. Mountain time is usually -7 UTC but during the summer it's effectively -6 UTC. Is there a way to have that list correctly reflect that?


Solution

  • TL;DR

    The time zone data you are receiving is "correct" because ActiveSupport::TimeZone and TZInfo::Timezone instances do not make assumptions about the current date, and therefore applying DST to them doesn't make "sense" in the context of the responsibility of those objects.

    You notice the problem because the default model for time_zone_options_for_select, ActiveSupport::TimeZone, unfortunately returns the offset string when calling #to_s, which, if the location is currently is observing DST, will be incorrect. There is no way to fix the string values generated in the options, or even remove the offset from them.

    If this isn't acceptable you'll need to skip on using time_zone_options_for_select, and use options_for_select instead, and generate the options yourself.

    Some investigation

    time_zone_options_for_select uses ::ActiveSupport::TimeZone as the default model parameter, so passing it in manually will not change your results. In order to construct options for a select box, that method will construct an array of tuples in the format of [time_zone.to_s, time_zone.name], for the purpose of passing it to the more generic options_for_select method. time_zone, in this case, is an instance of ::ActiveSupport::TimeZone.

    The important factor here is that this time zone instance object is, conceptually, completely unrelated/divorced from the idea of "the current date". The definition of a time zone (strictly speaking) has nothing to do with the current date. We can confirm this "not using DST" issue like so:

    ::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.utc_offset
    => 34200 # 9 hours and 30 minutes, in seconds
    

    Adelaide's non-DST time zone is ACST (Australian Central Standard Time) which is GMT+9.5. Currently (as in the time of writing), Adelaide is in DST which means they are on ACDT (Australian Central Daylight Time), which is GMT+10.5.

    ::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.now.utc_offset
    => 37800 # 10 hours and 30 minutes, in seconds
    

    The crucial difference here is essentially what I've outlined above - the ActiveSupport::TimeZone instance is just not concerned with the current date. The class itself is a convenience wrapper around a TZInfo::DataTimezone instance, which has similar opinions on the current date - none.

    If you noticed, in the second code snippet above, we called #now on the time zone object before calling #utc_offset. This returns an ActiveSupport::TimeWithZone instance, which includes information about the time zone, as well as the current date - and therefore we get an offset which reflects the fact that the current date should include the DST offset.

    So, the only problem here is that including the UTC offset string in the return value of #to_s on ActiveSupport::TimeZone instances is sort of misleading in this instance. It's included because it's the "base" UTC offset for that time zone.

    Resources: