Displaying local time in the correct timezone
Aug 25, 2010
Dick Brouwer
3 minute read

I’m working on a site called localhours.com that displays store opening hours across the US. Although previously I just used the browser’s client time and opening hour data in local time to display if a store was open or closed, this was obviously not sufficient (and tricky to do, since a browser request often doesn’t include a timestamp).

What makes this an interesting problem?

  • Timezones run through states and possibly through cities. At least a zipcode-timezone mapping is required.

  • A small portion of zipcodes are constantly changing apparently, and there is no database that can guarantee a 100% match between a zipcode and a timezone (and least for free). There are some data sets that provide ~98% accurate content though (free), so I spend some time figuring out which one to download. I settled on this link.

  • Not all states (zipcodes?) treat daylight savings in the same way. Luckily, since 2007 (in the US), DST starts at the same time everywhere: 2am on the second Sunday in March, and ends at 2am DST on the first Sunday of November. However, there are a few states that don’t have daylight savings.

In the end I:

  1. stored the local store timezone (tz_offset) along with bool (dst) indicating if daylight savings apply or not in the db,
  2. calculed the local time using datetime.utcnow() + tz_offset, and
  3. ran the result through a helper function that checked if dst should be applied for this time of the year.

The following module helped with the latter (mostly taken from the Python datetime docs):

from datetime import timedelta, datetime

def first_sunday_on_or_after(dt):
    days_to_go = 6 - dt.weekday()
    if days_to_go:
        dt += timedelta(days_to_go)
    return dt

# For a complete and up-to-date set of DST rules and timezone definitions,
# visit the Olson Database: http://www.twinsun.com/tz/tz-link.htm or
# http://sourceforge.net/projects/pytz/

# In the US, since 2007, DST starts at 2am (standard time) on the second
# Sunday in March, which is the first Sunday on or after Mar 8.
DSTSTART_2007 = datetime(1, 3, 8, 2)
# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
DSTEND_2007 = datetime(1, 11, 1, 1)
# From 1987 to 2006, DST used to start at 2am (standard time) on the first
# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
# Sunday of October, which is the first Sunday on or after Oct 25.
DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
DSTEND_1987_2006 = datetime(1, 10, 25, 1)
# From 1967 to 1986, DST used to start at 2am (standard time) on the last
# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
# 1am standard time) on the last Sunday of October, which is the first Sunday
# on or after Oct 25.
DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
DSTEND_1967_1986 = DSTEND_1987_2006

def dst(dt, dst):
    if not dst:
        return dt
    # Find start and end times for US DST. For years before 1967, return dt
    if 2006 < dt.year:
        dststart, dstend = DSTSTART_2007, DSTEND_2007
    elif 1986 < dt.year < 2007:
        dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
    elif 1966 < dt.year < 1987:
        dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
    else:
        return dt 

    start = first_sunday_on_or_after(dststart.replace(year=dt.year))
    end = first_sunday_on_or_after(dstend.replace(year=dt.year))

    if start <= dt < end:
        return timedelta(hours=1) + dt
    else:
        return dt


comments powered by Disqus