Last Updated: February 25, 2016
·
4.511K
· katylava

Datetimes and Timezones and DST, oh my!

This is an example of how to generate occurrences from python-dateutil's
rrule module, and convert them properly to UTC before saving to the
database, particularly if your input is localized to a timezone that
observes daylight savings.

Note: US daylight savings in 2014 starts on March 9

import pytz
from dateutil.parser import *
from dateutil.rrule import rrule, WEEKLY, MO


tz = pytz.timezone('America/Chicago')
start = parse('Feb 15 2014, 11am')
end = parse('March 24 2014')

_dates = rrule(WEEKLY, dtstart=start, until=end, byweekday=(MO,))
dates = list(_dates)

print dates

out:

[datetime.datetime(2014, 2, 17, 11, 0), 
 datetime.datetime(2014, 2, 24, 11, 0), 
 datetime.datetime(2014, 3, 3, 11, 0), 
 datetime.datetime(2014, 3, 10, 11, 0), 
 datetime.datetime(2014, 3, 17, 11, 0)]

These are naive datetimes, which is good, because we want them to be consitently
at 11am regardless of whether we're in daylight savings time or not. Now we can
localize each one to US Central and they will all stay at 11am.

localized = [tz.localize(dt) for dt in dates]
for dt in localized:
    print 'Central: {}; UTC: {}'.format(dt, pytz.utc.normalize(dt))

out:

Central: 2014-02-17 11:00:00-06:00; UTC: 2014-02-17 17:00:00+00:00
Central: 2014-02-24 11:00:00-06:00; UTC: 2014-02-24 17:00:00+00:00
Central: 2014-03-03 11:00:00-06:00; UTC: 2014-03-03 17:00:00+00:00
Central: 2014-03-10 11:00:00-05:00; UTC: 2014-03-10 16:00:00+00:00
Central: 2014-03-17 11:00:00-05:00; UTC: 2014-03-17 16:00:00+00:00

The left is what should be displayed to the user, but the right is what should
be saved to the database. However, you can't convert directly to UTC, because
this is what would happen:

utcified = [pytz.utc.localize(dt) for dt in dates]
for dt in utcified:
    print 'Central: {}; UTC: {}'.format(tz.normalize(dt), dt)

out:

Central: 2014-02-17 05:00:00-06:00; UTC: 2014-02-17 11:00:00+00:00
Central: 2014-02-24 05:00:00-06:00; UTC: 2014-02-24 11:00:00+00:00
Central: 2014-03-03 05:00:00-06:00; UTC: 2014-03-03 11:00:00+00:00
Central: 2014-03-10 06:00:00-05:00; UTC: 2014-03-10 11:00:00+00:00
Central: 2014-03-17 06:00:00-05:00; UTC: 2014-03-17 11:00:00+00:00

The obvious problem is our 11am local event is now scheduled for 5pm. However,
assuming for a moment that what we really wanted was an event to occure at 5pm
US Central every Monday, you can see that now when DST goes into effect, it gets
changed to 6pm, which is not what we want.

Steps to generate database-persistant event occurrences from a recurring rule respecting DST

  1. Request user input includes a time zone preference (or for local events, ensure you have a constant which stores the local timezone)
  2. Localize the naive start/end dates and the naive rrule-generated dates to the configured timezone. It's possible your start/end dates will have tzinfo on them (if it's coming from a Django form, for example), and you may need to remove that.
  3. Use pytz's normalize method to convert the dates to UTC and save to the database

Steps to display occurrences retrieved from database in the appropriate timezone

  1. Create a pytz timezone object for the user's or site's configured timezone
  2. Call the timezone object's normalize method, passing each UTC date in, to get the local representation