Search code examples
pythonutctimezone-offsetpytz

"Canonical" offset from UTC using pytz?


I am curious about how to call the "canonical" timezone offset that is offered on certain timezone selectors (if there's even such a thing, as a canonical offset, which I'm not even sure of).

For instance, on Windows XP, you can see that for Eastern Time (US & Canada) the drop-down always shows GMT-05:00 This is actually not correct for the whole year, since when daylight savings time is in effect, the offset with UTC is only -4:00. On the other hand, everyone mentions US/Eastern as being 5 hours off from UTC. I was wondering how that -5:00 is called. Email Time Identifier? Canonical timezone offset?

Also, if I create a US/Eastern timezone with pytz, is there a way of grabbing that -5:00 regardless of whether the actual now time is in DST or not? I mean... I'd like to know if there's a function, or... something to get -5:00, regardless of whether I'm running that today or in the middle of August (when DST is enabled and the actual offset is just -4:00)

Windows XP timezone selector

Image from http://www.microsoft.com/library/media/1033/windowsxp/images/using/setup/tips/67446-change-time-zone.gif

Thank you in advance.


Solution

  • If we take "canonical" to mean the utcoffset of dates that are not in DST, then the problem is reduced to finding dates (for each timezone) which are not DST.

    We could try the current date first. If it is not DST, then we are in luck. If it is, then we could step through the list of utc transition dates (which are stored in tzone._utc_transition_times) until we find one that is not DST:

    import pytz
    import datetime as DT
    utcnow = DT.datetime.utcnow()
    
    canonical = dict()
    for name in pytz.all_timezones:
        tzone = pytz.timezone(name)
        try:
            dstoffset = tzone.dst(utcnow, is_dst=False)
        except TypeError:
            # pytz.utc.dst does not have a is_dst keyword argument
            dstoffset = tzone.dst(utcnow)
        if dstoffset == DT.timedelta(0):
            # utcnow happens to be in a non-DST period
            canonical[name] = tzone.localize(utcnow, is_dst=False).strftime('%z') 
        else:
            # step through the transition times until we find a non-DST datetime
            for transition in tzone._utc_transition_times[::-1]:
                dstoffset = tzone.dst(transition, is_dst=False) 
                if dstoffset == DT.timedelta(0):
                    canonical[name] = (tzone.localize(transition, is_dst=False)
                                       .strftime('%z'))
                    break
    
    for name, utcoffset in canonical.iteritems():
        print('{} --> {}'.format(name, utcoffset)) 
    
    # All timezones have been accounted for
    assert len(canonical) == len(pytz.all_timezones)
    

    yields

    ...
    Mexico/BajaNorte --> -0800
    Africa/Kigali --> +0200
    Brazil/West --> -0400
    America/Grand_Turk --> -0400
    Mexico/BajaSur --> -0700
    Canada/Central --> -0600
    Africa/Lagos --> +0100
    GMT-0 --> +0000
    Europe/Sofia --> +0200
    Singapore --> +0800
    Africa/Tripoli --> +0200
    America/Anchorage --> -0900
    Pacific/Nauru --> +1200
    

    Note that the code above accesses the private attribute tzone._utc_transition_times. This is an implementation detail in pytz. Since it is not part of the public API, it is not guaranteed to exist in future versions of pytz. Indeed, it does not even exist for all timezones in the current version of pytz -- in particular, it does not exist for timezones that have no DST transition times, such as 'Africa/Bujumbura' for example. (That's why I bother to check if utcnow happens to be in a non-DST time period first.)

    If you'd like a method which does not rely on private attributes, we could instead simply march utcnow back one day until we find a day which is in a non-DST time period. The code would be a bit slower than the one above, but since you really only have to run this code once to glean the desired information, it really should not matter.

    Here is what the code would look like without using _utc_transition_times:

    import pytz
    import datetime as DT
    utcnow = DT.datetime.utcnow()
    
    canonical = dict()
    for name in pytz.all_timezones:
        tzone = pytz.timezone(name)
        try:
            dstoffset = tzone.dst(utcnow, is_dst=False)
        except TypeError:
            # pytz.utc.dst does not have a is_dst keyword argument
            dstoffset = tzone.dst(utcnow)
        if dstoffset == DT.timedelta(0):
            # utcnow happens to be in a non-DST period
            canonical[name] = tzone.localize(utcnow, is_dst=False).strftime('%z') 
        else:
            # step through the transition times until we find a non-DST datetime
            date = utcnow
            while True:
                date = date - DT.timedelta(days=1)
                dstoffset = tzone.dst(date, is_dst=False) 
                if dstoffset == DT.timedelta(0):
                    canonical[name] = (tzone.localize(date, is_dst=False)
                                       .strftime('%z'))
                    break
    
    for name, utcoffset in canonical.iteritems():
        print('{} --> {}'.format(name, utcoffset)) 
    
    # All timezones have been accounted for
    assert len(canonical) == len(pytz.all_timezones)