rubydatetimetimezonetimezone-offsettzinfo

Get UTC offset for Timezone at given date via Ruby/tzinfo?


This is probably trivial for anybody who knows the tzinfo API:

Given a Timezone object from tzinfo, how can I get the UTC offset at a given point in time (given either in local time of the Timezone or UTC)?


Solution

  • You can use the period_for_local method. For these examples, I'm using the timezone I live in (America/Sao_Paulo), in where the offset is -03:00 during winter (March to October) and -02:00 during summer (Daylight Saving Time):

    # Sao Paulo timezone
    zone = TZInfo::Timezone.new('America/Sao_Paulo')
    
    # date in January (Brazilia Summer Time - DST)
    d = DateTime.new(2017, 1, 1, 10, 0)
    
    period = zone.period_for_local(d)
    puts period.offset.utc_total_offset / 3600.0
    
    # date in July (Brazilia Standard Time - not in DST)
    d = DateTime.new(2017, 7, 1, 10, 0)
    
    period = zone.period_for_local(d)
    puts period.offset.utc_total_offset / 3600.0
    

    The output is:

    -2.0
    -3.0

    The utc_total_offset method returns the offset in seconds, so I divided by 3600 to get the value in hours.

    Note that I also used 3600.0 to force the results to be a float. If I just use 3600, the results will be rounded and timezones like Asia/Kolkata (which has an offset of +05:30) will give incorrect results (5 instead of 5.5).


    Note that you must be aware of DST changes, because you can have either a gap or a overlap.

    In São Paulo timezone, DST starts at October 15th 2017: at midnight, clocks shift forward to 1 AM (and offset changes from -03:00 to -02:00), so all the local times between 00:00 and 01:00 are not valid. In this case, if you try to get the offset, it will get a PeriodNotFound error:

    # DST starts at October 15th, clocks shift from midnight to 1 AM
    d = DateTime.new(2017, 10, 15, 0, 30)
    period = zone.period_for_local(d) # error: TZInfo::PeriodNotFound
    

    When DST ends, at February 18th 2018, at midnight clocks shift back to 11 PM of 17th (and offset changes from -02:00 to -03:00), so the local times between 11 PM and midnight exist twice (in both offsets).

    In this case, you must specify which one you want (by setting the second parameter of period_for_local), indicating if you want the offset for DST or not:

    # DST ends at February 18th, clocks shift from midnight to 11 PM of 17th
    d = DateTime.new(2018, 2, 17, 23, 30)
    period = zone.period_for_local(d, true) # get DST offset
    puts period.offset.utc_total_offset / 3600.0 # -2.0
    
    period = zone.period_for_local(d, false) # get non-DST offset
    puts period.offset.utc_total_offset / 3600.0 # -3.0
    

    If you don't specify the second parameter, you'll get a TZInfo::AmbiguousTime error:

    # error: TZInfo::AmbiguousTime (local time exists twice due do DST overlap)
    period = zone.period_for_local(d)