This class implements the daylight saving time rules for the USA, honoring the change that went into effect in 2007. Dates before 2007 are assumed to use the old rules.
Daylight time always changes on a Sunday. To compute the
dates of those Sundays in a particular year, we will need the
following function. Given a datetime.date
instance dt, it computes the date of
the first Sunday on or after that date, and returns
the result as a datetime.date.
def firstSundayOnOrAfter ( dt ):
"""Find the first Sunday on or after a given date.
[ dt is a datetime.date ->
return a datetime.date representing the first Sunday
on or after dt ]
"""
Because the .weekday() method returns a number
that represents Sunday as 6, we subtract that number from 6 to
get the number of days between dt and the next Sunday.
Then we use a datetime.timedelta instance
to add to dt to yield the Sunday date.
daysToGo = dt.weekday()
if daysToGo:
dt += datetime.timedelta ( daysToGo )
return dt
Here's the class itself.
# - - - - - c l a s s U S T i m e Z o n e
class USTimeZone(datetime.tzinfo):
"""Represents a U.S. time zone, with automatic daylight time.
Exports:
USTimeZone ( hh, mm, name, stdName, dstName ):
[ (hh is an offset east of UTC in hours) and
(mm is an offset east of UTC in minutes) and
(name is the composite zone name) and
(stdName is the non-DST name) and
(dstName is the DST name) ->
return a new USTimeZone instance with those values ]
State/Invariants:
.__offset:
[ self's offset east of UTC as a datetime.timedelta ]
.__name: [ as passed to constructor's name ]
.__stdName: [ as passed to constructor's stdName ]
.__dstName: [ as passed to constructor's dstName ]
"""
This class is an adaptation of the USTimeZone
class shown in the examples section of the
reference material for datetime.tzinfo, with modifications for the new daylight time rules.
We'll need class variables for the calculation of the DST
changeover dates. In each case, we want a date such that we
can plug in the given year and send it to the firstSundayOnOrAfter() function above to yield a DST
changeover date.
DST_START_OLD: The pre-2007 rule for DST is
that it starts on the first Sunday in April, at 2am.
Hence, this date is April 1. Applying firstSundayOnOrAfter() to this date gives us the
first Sunday in April at 2am.
DST_START_OLD = datetime.datetime ( 1, 4, 1, 2 )
DST_END_OLD: The pre-2007 rule is that DST
ends on the last Sunday of October, that is, the first
Sunday on or after October 25.
DST_END_OLD = datetime.datetime ( 1, 10, 25, 2 )
DST_START_2007: The new rule is that DST
starts on the second Sunday in March, that is, the first
Sunday on or after March 8.
DST_START_2007 = datetime.datetime ( 1, 3, 8, 2 )
DST_END_2007: Since 2007, DST ends on the
first Sunday in November.
DST_END_2007 = datetime.datetime ( 1, 11, 1, 2 )
Here's the class constructor.
def __init__ ( self, hh, mm, name, stdName, dstName ):
self.__offset = datetime.timedelta ( hours=hh, minutes=mm )
self.__name = name
self.__stdName = stdName
self.__dstname = dstName
The .tzname() method returns the current name,
which depends on whether the given time is DST or not.
def tzname(self, dt):
if self.dst(dt): return self.__dstName
else: return self.__stdName
The .utcoffset() method returns the offset east
of UTC, taking into account whether DST is in effect. We
use the fact that a datetime.timedelta with a
zero offset is treated as false in Boolean contexts.
def utcoffset(self, dt):
return self.__offset + self.dst(dt)
The .dst() method returns an offset as a datetime.timedelta, and determines whether DST is in effect.
def dst(self, dt):
"""Return the current DST offset.
[ dt is a datetime.date ->
if daylight time is in effect in self's zone on
date dt ->
return +1 hour as a datetime.timedelta
else ->
return 0 as a datetime.delta ]
"""
This implementation will ignore the .tzinfo
attribute of the dt argument, and just use
self's zone information.
First we must find the starting and ending Sundays for DST for
the year given by dt. The .replace() method gives us a new datetime instance with the year taken from dt.
#-- 1 --
# [ dtStart := Sunday when DST starts in year dt.year
# dtEnd := Sunday when DST ends in year dt.year ]
if dt.year >= 2007:
startDate = self.DST_START_2007.replace ( year=dt.year )
endDate = self.DST_END_2007.replace ( year=dt.year )
else:
startDate = self.DST_START_OLD.replace ( year=dt.year )
endDate = self.DST_END_OLD.replace ( year=dt.year )
dtStart = firstSundayOnOrAfter ( startDate )
dtEnd = firstSundayOnOrAfter ( endDate )
The datetime module does not allow naive times
to be compared to aware times, so we'll make a copy of dt with its time zone information removed.
#-- 2 --
# [ naiveDate := dt with its tzinfo member set to None ]
naiveDate = dt.replace ( tzinfo=None )
Now we can find out if naiveDate is within the
DST range, and return the appropriate offset.
#-- 3 --
# [ if naiveDate is in the interval (dtStart, dtEnd) ->
# return DELTA_HOUR
# else ->
# return DELTA_ZERO ]
if dtStart <= naiveDate < dtEnd:
return DELTA_HOUR
else:
return DELTA_ZERO