#
# xcal.py
# Manipulation for calendar values from year 0 to 100000 and
# beyond.
#
# Based on a library in C:
#
# Copyright (c) 1997 Wolfgang Helbig
# All rights reserved.
#
# Converted to Python by Andy Valencia 11/2008
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
import time
#
# For each month tabulate the number of days elapsed in a year before the
# month. This assumes the internal date representation, where a year
# starts on March 1st. So we don't need a special table for leap years.
# But we do need a special table for the year 1582, since 10 days are
# deleted in October. This is month1s for the switch from Julian to
# Gregorian calendar.
#
month1 = \
(0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337)
# M A M J J A S O N D J
month1s = \
(0, 31, 61, 92, 122, 153, 184, 214, 235, 265, 296, 327)
# The last day of Julian calendar, in internal and ndays representation
# Dates in this format are stored in a tuple as (year, month, day)
nswitch = None
jiswitch = (1582, 7, 3)
# Names of days of week
daynames = ("Monday", "Tuesday", "Wednesday", "Thursday", \
"Friday", "Saturday", "Sunday")
#
# Class wrapper around a (y, m, d) tuple
#
# y, m, d - Year month and day for this Date
#
class Date():
def __init__(self, year, month, day):
self.y = year
self.m = month
self.d = day
self.wday = None
self.nmonth = None
# Sring rep
def __str__(self):
return "%s/%s/%s" % (self.m, self.d, self.y)
# Comparisons
def __eq__(self, other):
if other == None:
return False
return (self.y == other.y) and (self.m == other.m) \
and (self.d == other.d)
def __ne__(self, other):
if other == None:
return True
return not (self == other)
def __lt__(self, other):
if self.y > other.y: return False
if self.y < other.y: return True
if self.m > other.m: return False
if self.m < other.m: return True
return self.d < other.d
def __le__(self, other):
return (self == other) or (self < other)
def __gt__(self, other):
return (not (self == other)) and (not (self < other))
def __ge__(self, other):
return (self == other) or (self > other)
# Return the number of days since March 1st of the year zero.
# The date is given according to Julian calendar.
def ndaysj(self):
idt = self.date2idt()
return idt.ndaysji()
# Same as above, where the Julian date is given in internal notation.
# This formula shows the beauty of this notation.
def ndaysji(self):
return self.d + month1[self.m] + (self.y * 365) + (self.y / 4)
# Return the number of days since March 1st of the year zero. The date is
# assumed Gregorian if younger than 1582-10-04 and Julian otherwise. This
# is the reverse of gdate.
def ndaysg(self):
idt = self.date2idt()
return idt.ndaysgi()
# Same as above, but with the Gregorian date given in internal
# representation.
def ndaysgi(self):
global nswitch
# Cache nswitch if not already done
if nswitch == None:
nswitch = Date(*jiswitch).ndaysji()
# Assume Julian calendar and adapt to Gregorian if necessary, i. e.
# younger than nswitch. Gregori deleted
# the ten days from Oct 5th to Oct 14th 1582.
# Thereafter years which are multiples of 100 and not multiples
# of 400 were not leap years anymore.
# This makes the average length of a year
# 365d +.25d - .01d + .0025d = 365.2425d. But the tropical
# year measures 365.2422d. So in 10000/3 years we are
# again one day ahead of the earth. Sigh :-)
# (d is the average length of a day and tropical year is the
# time from one spring point to the next.)
#
# "nd" is number of days--return value
nd = self.ndaysji()
if self.y >= 1600:
nd = (nd - 10 - (self.y - 1600) / 100 + (self.y - 1600) / 400)
elif nd > nswitch:
nd -= 10
return (nd);
# Convert a date to internal date representation: The year starts on
# March 1st, month and day numbering start at zero. E. g. March 1st of
# year zero is written as y=0, m=0, d=0.
def date2idt(self):
d = self.d - 1
if self.m > 2:
m = self.m - 3
y = self.y
else:
m = self.m + 9
y = self.y - 1
assert not ((m < 0) or (m > 11) or (y < 0))
return Date(y, m, d)
# Reverse of date2idt
def idt2date(self):
d = self.d + 1;
if self.m < 10:
m = self.m + 3
y = self.y
else:
m = self.m - 9
y = self.y + 1
assert m >= 1
return Date(y, m, d)
# Tell our weekday
def weekday(self):
# Cache it on first reference
if self.wday == None:
self.wday = weekday(self.ndaysg())
return self.wday
# Tell name of weekday
def dayname(self):
return daynames[self.weekday()]
# Tell number of days to reach 1st of next month
def days2next(self):
# (The answer is cached)
if self.nmonth == None:
y = self.y
m = self.m+1
if m > 12:
y += 1
m = 1
nd = Date(y, m, 1)
self.nmonth = nd.ndaysj() - self.ndaysj()
return self.nmonth
# Compute the Julian date from the number of days elapsed since
# March 1st of year zero.
def jdate(ndays):
#
# Compute the year by starting with an approximation not smaller
# than the answer and using linear search for the greatest
# year which does not begin after ndays.
#
# "idt" is our internal date representation,
# "r" holds the rest of days
#
idt = Date(ndays / 365, 0, 0)
while True:
r = idt.ndaysji()
if r <= ndays:
break
idt.y -= 1
# Set r to the days left in the year and compute the month by
# linear search as the largest month that does not begin after r
# days.
r = ndays - r
idt.m = 11
while month1[idt.m] > r:
idt.m -= 1
# Compute the days left in the month
idt.d = r - month1[idt.m]
# Return external representation of the date
return idt.idt2date()
# Compute the date according to the Gregorian calendar from the number of
# days since March 1st, year zero. The date computed will be Julian if it
# is older than 1582-10-05. This is the reverse of the function ndaysg().
def gdate(ndays):
# Compute the year by starting with an approximation not smaller
# than the answer and search linearly for the greatest year not
# starting after ndays.
# "idt" is our internal date representation,
# "r" holds the rest of days
idt = Date(ndays / 365, 0, 0)
while True:
r = idt.ndaysgi()
if r <= ndays:
break
idt.y -= 1
# Set ndays to the number of days left and compute by linear
# search the greatest month which does not start after ndays. We
# use the table month1 which provides for each month the number
# of days that elapsed in the year before that month. Here the
# year 1582 is special, as 10 days are left out in October to
# resynchronize the calendar with the earth's orbit. October 4th
# 1582 is followed by October 15th 1582. We use the "switch"
# table month1s for this year.
ndays = ndays - r
if idt.y == 1582: montht = month1s
else: montht = month1
idt.m = 11
while montht[idt.m] > ndays:
idt.m -= 1
# The rest is the day in month
idt.d = ndays - montht[idt.m]
# Advance ten days deleted from October if after switch in Oct 1582
if (idt.y == jiswitch[0]) and (idt.m == jiswitch[1]) and \
(jiswitch[2] < idt.d):
idt.d += 10;
# Return external representation of found date
return idt.idt2date()
# Compute the week number from the number of days since March 1st year 0.
# The weeks are numbered per year starting with 1. If the first
# week of a year includes at least four days of that year it is week 1,
# otherwise it gets the number of the last week of the previous year.
# Return value is (week #, Y)
# Where Y is the year that contains the greater part of the week
def week(nd):
dt = gdate(nd)
y = dt.y + 1
while True:
fw = firstweek(y)
if nd >= fw:
break
y -= 1
return ((nd - fw) / 7 + 1, y)
# Return the first day of week 1 of year y
def firstweek(y):
# Internal representation of y-1-1
idt = Date(y - 1, 10, 0)
# If more than 3 days of this week are in the preceding year, the
# next week is week 1 (and the next monday is the answer),
# otherwise this week is week 1 and the last monday is the
# answer.
nd = idt.ndaysgi()
wd = weekday(nd)
if wd > 3:
return nd - wd + 7
return nd - wd
# Cache daynumber of one Monday
nmonday = None
# Return the weekday (Mo = 0 .. Su = 6)
def weekday(nd):
global nmonday
# Cache the daynumber of one monday
if nmonday == None:
# Internal repr. of 1997-11-17
dmondaygi = Date(1997, 8, 16)
nmonday = dmondaygi.ndaysgi()
# Return (nd - nmonday) modulo 7 which is the weekday
nd = (nd - nmonday) % 7
if nd < 0:
return nd + 7
return nd
# Return a Date() for today
def today():
return Date(*time.localtime()[0:3])
# Parse M/D/Y into a Date
def parse(str):
# TBD is use a stronger parser than time.strptime()
tm = time.strptime(str, "%m/%d/%Y")
return Date(*tm[0:3])