Savir
Savir

Reputation: 18418

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

Upvotes: 4

Views: 1629

Answers (1)

unutbu
unutbu

Reputation: 879083

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)

Upvotes: 3

Related Questions