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:
- stored the local store timezone (tz_offset) along with bool (dst) indicating if daylight savings apply or not in the db,
- calculed the local time using datetime.utcnow() + tz_offset, and
- 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