Saturday, July 18, 2009

Python Date class

I doubt whether the world really needs another one, but here goes. This post contains code for a Date class in Python. The class instances know how to calculate the day of the week, and they also know how to calculate the number of days separating two dates. This is all pretty standard.

What may be of interest is the method. The fundamental data structure is a list containing one element for each year since the year 1, with an extra 0 at the beginning to allow use of 1-based indexing. Each element in this list is itself a list of the number of days for each month of that particular year. We calculate this once, when the first Date object is instantiated. (Unless a date beyond 2050 is desired, then the list is extended as far as needed). We also cache the total number of days for each year in another list, to reduce calculation.

Using these two lists, it is trivial to calculate for any date the number of days since Jan 1 of the year 1 (a Saturday), and then, the day of the week is a simple mod 7 operation.

Here is output from the Unix cal function:

$ cal 9 1752
September 1752
Su Mo Tu We Th Fr Sa
1 2 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30


The month of September in the year 1752 was special (in Great Britain and the United States, at least). It is the month from which extra days were dropped in order to bring the calendar back into register with the earth's actual position, before changing from the Julian to the Gregorian calendar. (In Catholic Europe the change was made in October 1582, and only ten days were dropped, from 5 October 1582 to 14 October 1582).

We deal with this simply by making L[1752][9] = 19.

Here is a test function that exercises the class:

def test():
t1 = (1,1,1)
t2 = (7,4,1776)
t3 = (4,15,1865)
t4 = (7,20,1969)
t5 = (7,19,2009)
t6 = (7,19,2637)

L = [t1,t2,t3,t4,t5,t6]
for t in L:
d = Date(t)
print d.wd, d

d1 = Date(t5)
d2 = Date(t4)
print d1 - d2


And the output:

Sat Jan 01,   1
Thu Jul 04,1776
Sat Apr 15,1865
Sun Jul 20,1969
Sun Jul 19,2009
Tue Jul 19,2637
14609


14,609 days since the first moonwalk. I watched on TV!

I do hope our descendants are still here in 2637. Update: there seems to be a bug, either in my code or the cal function. It shows July 19, 2637 as a Wed. All the other dates check out. Ideas?

The entire listing:

class Date:
# 4 class variables
mlen = [0,31,28,31,30,31,30,
31,31,30,31,30,31 ]
day = {1:'Sat',2:'Sun',
3:'Mon',4:'Tue',
5:'Wed',6:'Thu',
7:'Fri'}
# a list of month lengths, indexed by year
yL = None
# the sum of yL elements, indexed by year
ylen = None

def __init__(self,t):
m,d,y = t
self.month = m
self.day = d
self.year = y
if not Date.yL:
Date.yL = self.populateYList()
if y > 2050:
Date.yL = self.populateYList(y+1)
if not Date.ylen:
Date.ylen = [
sum(mL) for mL in Date.yL]
self.n = self.daynumber()
self.wd = self.dayofweek()

def populateYList(self,N=2050):
yL = [[0]]
for year in range(1,N):
months = Date.mlen[:]
if year < 1752: # Julian
if not year % 4:
months[2] = 29
elif year == 1752: # special
months[2] = 29
months[9] = 19
else: # Gregorian
if not year % 400:
months[2] = 29
elif not year % 100:
pass
elif not year % 4:
months[2] = 29
yL.append(months)
return yL

def daynumber(self):
# should guard against Sep 1752
n = self.day
n += sum(Date.ylen[:self.year])
mlen = Date.yL[self.year]
n += sum(mlen[:self.month])
return n

def dayofweek(self):
#01 Jan of year 1 was a Sat
r = self.n % 7
return Date.day[r]

def __sub__(self,other):
n1 = self.n
n2 = other.n
if n2 > n1: return n2 - n1
return n1 - n2

def __repr__(self):
D = [0,'Jan','Feb','Mar','Apr',
'May','Jun','Jul','Aug',
'Sep','Oct','Nov','Dec']
y = str(self.year)
d = self.day
if d < 10: sd = '0' + str(d)
else: sd = str(d)
s = D[self.month] + ' '
return s + sd + ',' + y.rjust(4)
#=============================

def test():
t1 = (1,1,1)
t2 = (7,4,1776)
t3 = (4,15,1865)
t4 = (7,20,1969)
t5 = (7,19,2009)
t6 = (7,19,2637)

L = [t1,t2,t3,t4,t5,t6]
for t in L:
d = Date(t)
print d.wd, d

d1 = Date(t5)
d2 = Date(t4)
print d1 - d2

if __name__ == '__main__':
test()