Date-Time type (was Re: [DB-SIG] DB-API Spec. 1.1a1)
Christian Egli
christian.egli@stest.ch
08 Dec 1997 14:12:53 +0100
>>>>> "Marc" == M -A Lemburg <lemburg@uni-duesseldorf.de> writes:
Marc> As Bill and Jim already pointed out, the current date/time
Marc> handling in Python is fine when all you care about is a range
Marc> from 1970 to 2038, but as soon as you want to handle dates
Marc> beyond that range, you get into trouble.
Marc> BTW: I'm working on a prototype for a such date type in
Marc> Python. I'm not yet sure, but it seems that actually two
Marc> types will emerge from it: one for fixed points in time and
Marc> one to express time deltas.
I can't hold myself back any longer:
Edward M. Reingold has written some fine functions relating to
calendars which are included in the Emacs distribution. This code has
been translated by himself to C++ and can be found under
http://emr.cs.uiuc.edu/~reingold/papers/calendar/calendar.C. I tried
to swig this code but had troubles with overloaded operators. But it
was pretty straightforward to translate it into Python.
Only a few of the requirements for a Date-Time type that Jim Fulton
once stated in the db-sig are actually supported and obviously it is
not implemented in C. But maybe some of this code could be scavenged
for a C implementation or somebody manages to swig the above C++ code.
This code is released as is. "It works for me". I don't claim it to be
complete. Nor do I claim that it is the answer (or even an answer) to
the current discussion in the db-sig. See also the TODO section in the
code.
----------------cut here--------------------------------------------------------
#!/bin/env python
"""The following python code is translated from C++ code which is
translated from the Lisp code in ``Calendrical Calculations'' by
Nachum Dershowitz and Edward M. Reingold, Software---Practice &
Experience, vol. 20, no. 9 (September, 1990), pp. 899--928. The C++
code can be obtained from
http://emr.cs.uiuc.edu/~reingold/papers/calendar/calendar.C
This code is in the public domain, but any use of it
should publically acknowledge its source.
Classes GregorianDate, JulianDate, IsoDate, IslamicDate,
and HebrewDate
TODO:
As Jim Fulton once noted in the db-sig:
> It would be helpful to have a standard date-time implementation for
> use in Python database implementations (and elsewhere, of course).
>
> I think that the date-time implementation should:
>
> 1. Support conversion from strings in a very wide variety of formats
> (e.g. 'Oct, 1, 1994 12:34am EST', '1994/10/1 00:34:21.456')
>
> 2. Support subtraction of date-times, and addition and
> subtraction of dates and numbers.
>
> 3. Store dates efficiently.
>
> 4. Store dates immutably.
>
> 5. Represent dates to a specified minimum precision
> (e.g. milliseconds).
>
> 6. Handle all dates in the Gregorian calendar. (e.g. there should
> not be problems storing dates from the 18th or 21st centurys.)
>
> 7. Provide read-access to date-components (e.g. year, month, second,
> day-of-week, etc.)
>
> I'm afraid the implementation should also address issues like:
>
> 8. Support for time-zones,
>
> 9. Support for daylight-savings time.
This implementation of Date only fullfills Requirement 2 and 6
and to some degree 7.
"""
__version__ = "1.5"
DayName = ("Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday")
# Absolute dates
class Date:
"""Absolute date means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
reprFormat = "%(month)s/%(day)s/%(year)s"
def __init__(self, absoluteDate = None):
self.abs = absoluteDate
def __repr__(self):
return self.reprFormat % self.__dict__
def __cmp__(self, other):
"""Comparison of Dates with Dates returns -1 if self < other,
0 if self == other or 1 if self > other.
Comparison of Dates with any other type raises a TypeError exception.
"""
if (type(other) == types.InstanceType and
issubclass(other.__class__, Date)):
if self.absolute() < other.absolute():
return -1
elif self.absolute() > other.absolute():
return 1
else:
return 0
else:
raise TypeError, 'Comparison of Dates is only allowed with Dates'
def __add__(self, other):
"""Addition of Dates with numbers (int and long) returns a new Date.
Addition of Dates with any other type raises a TypeError exception.
"""
import types
if type(other) in [types.IntType, types.LongType]:
return self.__class__(self.absolute() + other)
else:
raise TypeError, 'Addition of Dates is only allowed with numbers'
__radd__ = __add__
def __sub__(self, other):
"""Subtraction of Dates with numbers (int and long) returns a new Date.
Subtraction of Dates with Dates returns an absolute difference in days.
Subtraction of Dates with any other type raises a TypeError exception."""
import types
if (type(other) == types.InstanceType and
issubclass(other.__class__, Date)):
return self.absolute() - other.absolute()
elif type(other) in [types.IntType, types.LongType]:
return self.__class__(self.absolute() - other)
else:
raise TypeError, 'Subtraction of Dates is only allowed with Dates or numbers'
__rsub__ = __sub__
def XdayOnOrBefore(d, x):
"""Absolute date of the x-day on or before absolute date d.
x=0 means Sunday, x=1 means Monday, and so on.
"Absolute date" means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
return (d - ((d - x) % 7))
# Gregorian dates
def LastDayOfGregorianMonth(month, year):
"""Compute the last date of the month for the Gregorian calendar."""
if (month == 2):
if ((((year % 4) == 0) and ((year % 100) != 0)) or ((year % 400) == 0)):
return 29
else:
return 28
elif ((month == 4) or (month == 6) or (month == 9) or (month == 11)):
return 30
else:
return 31
def NthXday(n, x, month, year, day = 0):
"""The Gregorian date of nth x-day in month, year before/after optional day.
x = 0 means Sunday, x = 1 means Monday, and so on. If n<0, return the nth
x-day before month day, year (inclusive). If n>0, return the nth x-day
after month day, year (inclusive). If day is omitted or 0, it defaults
to 1 if n>0, and month's last day otherwise."""
if (n > 0):
if (day == 0):
day = 1 # default for positive n
absDate = GregorianDate(day, month, year).absolute()
return GregorianDate((7 * (n - 1)) + XdayOnOrBefore(6 + absDate, x))
else:
if (day == 0):
day = LastDayOfGregorianMonth(month, year) # default for negative n
absDate = GregorianDate(day, month, year).absolute()
return GregorianDate((7 * (n + 1)) + XdayOnOrBefore(absDate, x))
class GregorianDate(Date):
"""The Gregorian Calendar is the present day civil calendar in much of
the world. It was instituted by Pope Gregory when he corrected the
Julian Calendar by proclaiming that Thursday, October 4, 1582
C.E. would be followed by Friday, October 15, 1582 C.E. thus skipping
10 days. Over time most of the countries of world adopted this
calendar. It corrected for the problems of the Julian Calendar by
introducing a more complicated leap year structure: Year y is a leap
year if it is divisible by 400 or it is a year that is divisible by 4
and not divisible by 100.
"""
def __init__(self, d, m = 0, y = 0):
"""year # 1...
month # 1 == January, ..., 12 == December
day # 1..LastDayOfGregorianMonth(month, year)
"""
if ((m != 0) and (y != 0)):
Date.__init__(self)
self.month = m
self.day = d
self.year = y
else:
Date.__init__(self, d)
# Computes the Gregorian date from the absolute date.
# Search forward year by year from approximate year
year = d/366
while (d >= GregorianDate(1,1,year+1).absolute()):
year = year + 1
# Search forward month by month from January
month = 1
while (d > GregorianDate(LastDayOfGregorianMonth(month,year), month, year).absolute()):
month = month + 1
day = d - GregorianDate(1,month,year).absolute() + 1
self.year, self.month, self.day = year, month, day
def absolute(self):
"""Computes the absolute date from the Gregorian date.
"Absolute date" means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
if self.abs:
return self.abs
N = self.day # days this month
year = self.year
month = self.month
for m in range(month - 1, 0, -1): # days in prior months this year
N = N + LastDayOfGregorianMonth(m, year)
self.abs = (N # days this year
+ 365 * (year - 1) # days in previous years ignoring leap days
+ (year - 1)/4 # Julian leap days before this year...
- (year - 1)/100 # ...minus prior century years...
+ (year - 1)/400) # ...plus prior years divisible by 400
return self.abs
# Julian dates
JulianEpoch = -2 # Absolute date of start of Julian calendar
def LastDayOfJulianMonth(month, year):
"""Compute the last date of the month for the Julian calendar."""
if (month == 2):
if ((year % 4) == 0):
return 29
else:
return 28
elif ((month == 4) or (month == 6) or (month == 9) or (month == 11)):
return 30
else:
return 31
class JulianDate(Date):
"""The Julian Calendar was the predecessor to the Gregorian Calendar
as the civil calendar for much of the world. It has a very simple leap
year structure, all years divisible by 4 are leap years. This is close
to true but deviates from the average length of the solar year over
time. Thus the need for the 10 day correction in 1582.
"""
def __init__(self, d, m = 0, y = 0):
"""year # 1...
month # 1 == January, ..., 12 == December
day # 1..LastDayOfJulianMonth(month, year)
"""
if ((m != 0) and (y != 0)):
Date.__init__(self)
self.month = m
self.day = d
self.year = y
else:
Date.__init__(self, d)
# Computes the Julian date from the absolute date.
# Search forward year by year from approximate year
year = (d + JulianEpoch)/366
while (d >= JulianDate(1,1,year+1).absolute()):
year = year + 1
# Search forward month by month from January
month = 1
while (d > JulianDate(LastDayOfJulianMonth(month,year), month, year).absolute()):
month = month + 1
day = d - JulianDate(1,month,year).absolute() + 1
self.year, self.month, self.day = year, month, day
def absolute(self):
"""Computes the absolute date from the Julian date.
"Absolute date" means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
if self.abs:
return self.abs
N = self.day # days this month
year = self.year
for m in range(self.month - 1, 0, - 1): # days in prior months this year
N = N + LastDayOfJulianMonth(m, year)
self.abs = (N # days this year
+ 365 * (year - 1) # days in previous years ignoring leap days
+ (year - 1)/4 # leap days before this year...
+ JulianEpoch) # days elapsed before absolute date 1
return self.abs
# ISO dates
class IsoDate(Date):
"""The International Organization for Standardization (ISO) produced a
calendar that is popular in some European countries. A date is
specified as the ordinal day in the week and the `calendar week' of
the Gregorian year.
The `ISO year' corresponds approximately to the Gregorian year, but
weeks start on Monday and end on Sunday. The first week of the ISO year is
the first such week in which at least 4 days are in a year. The ISO
commercial DATE has the form (week day year) in which week is in the range
1..52 and day is in the range 0..6 (1 = Monday, 2 = Tuesday, ..., 0 =
Sunday).
"""
reprFormat = "%(week)s-%(day)s-%(year)s"
def __init__(self, d, w = 0, y = 0):
"""year # 1...
week # 1..52 or 53
day # 1..7
"""
if ((w != 0) and (y != 0)):
Date.__init__(self)
self.week = w
self.day = d
self.year = y
else:
Date.__init__(self, d)
# Computes the ISO date from the absolute date.
year = GregorianDate(d - 3).year
if (d >= IsoDate(1,1,year+1).absolute()):
year = year + 1
if ((d % 7) == 0):
day = 7 # Sunday
else:
day = d % 7 # Monday..Saturday
week = 1 + (d - IsoDate(1,1,year).absolute()) / 7
self.year, self.week, self.day = year, week, day
def absolute(self):
"""Computes the absolute date from the ISO date. """
if self.abs:
return self.abs
self.abs = (XdayOnOrBefore(GregorianDate(4,1,self.year).absolute(),1) # days in prior years
+ 7 * (self.week - 1) # days in prior weeks this year
+ (self.day - 1)) # prior days this week
return self.abs
# Islamic dates
IslamicEpoch = 227014 # Absolute date of start of Islamic calendar
def IslamicLeapYear(year):
"""True if year is an Islamic leap year"""
if ((((11 * year) + 14) % 30) < 11):
return 1
else:
return 0
def LastDayOfIslamicMonth(month, year):
"""Last day in month during year on the Islamic calendar."""
if (((month % 2) == 1) or ((month == 12) and IslamicLeapYear(year))):
return 30
else:
return 29
class IslamicDate(Date):
"""This Islamic Calendar is a strictly lunar calendar making it very easy
to calculate. No attempt is made to keep the months in line with the
seasons of the year. Instead they wander through the seasons as the
years go by. As in the Hebrew Calendar the day begins at sunset which
we again take to be 6pm local time. Unfortunately the calculations
provided here are only approximate. Unlike the Hebrew Calendar there
are many more disparate forms of the Islamic Calendar. Even worse,
much of the Islamic world still relies on proclamations of the new
moon by religious authorities instead of on calculations. Thus the
routines can be in error by a day or two from what is actually
observed in different parts of the Islamic world.
"""
def __init__(self, d, m = 0, y = 0):
"""year # 1...
month # 1..13 (12 in a common year)
day # 1..LastDayOfIslamicMonth(month,year)
"""
if ((m != 0) and (y != 0)):
Date.__init__(self)
self.month = m
self.day = d
self.year = y
else:
Date.__init__(self, d)
# Computes the Islamic date from the absolute date.
if (d <= IslamicEpoch): # Date is pre-Islamic
month = 0
day = 0
year = 0
else:
# Search forward year by year from approximate year
year = (d - IslamicEpoch) / 355
while (d >= IslamicDate(1,1,year+1).absolute()):
year = year + 1
# Search forward month by month from Muharram
month = 1
while (d > IslamicDate(LastDayOfIslamicMonth(month,year), month, year).absolute()):
month = month + 1
day = d - IslamicDate(1,month,year).absolute() + 1
self.year, self.month, self.day = year, month, day
def absolute(self):
"""Computes the absolute date from the Islamic date.
"Absolute date" means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
if self.abs:
return self.abs
self.abs = (self.day # days so far this month
+ 29 * (self.month - 1) # days so far...
+ self.month/2 # ...this year
+ 354 * (self.year - 1) # non-leap days in prior years
+ (3 + (11 * self.year)) / 30 # leap days in prior years
+ IslamicEpoch) # days before start of calendar
return self.abs
# Hebrew dates
HebrewEpoch = -1373429 # Absolute date of start of Hebrew calendar
def HebrewLeapYear(year):
"""True if year is an Hebrew leap year"""
if ((((7 * year) + 1) % 19) < 7):
return 1
else:
return 0
def LastMonthOfHebrewYear(year):
"""Last month of Hebrew year."""
if (HebrewLeapYear(year)):
return 13
else:
return 12
def HebrewCalendarElapsedDays(year):
"""Number of days elapsed from the Sunday prior to the start of the
Hebrew calendar to the mean conjunction of Tishri of Hebrew year."""
MonthsElapsed = ((235 * ((year - 1) / 19)) # Months in complete cycles so far.
+ (12 * ((year - 1) % 19)) # Regular months in this cycle.
+ (7 * ((year - 1) % 19) + 1) / 19) # Leap months this cycle
PartsElapsed = 204 + 793 * (MonthsElapsed % 1080)
HoursElapsed = (5 + 12 * MonthsElapsed + 793 * (MonthsElapsed / 1080)
+ PartsElapsed / 1080)
ConjunctionDay = 1 + 29 * MonthsElapsed + HoursElapsed / 24
ConjunctionParts = 1080 * (HoursElapsed % 24) + PartsElapsed % 1080
if ((ConjunctionParts >= 19440) # If new moon is at or after midday,
or (((ConjunctionDay % 7) == 2) # ...or is on a Tuesday...
and (ConjunctionParts >= 9924) # at 9 hours, 204 parts or later...
and not (HebrewLeapYear(year))) # ...of a common year,
or (((ConjunctionDay % 7) == 1) # ...or is on a Monday at...
and (ConjunctionParts >= 16789) # 15 hours, 589 parts or later...
and (HebrewLeapYear(year - 1)))): # at the end of a leap year
# Then postpone Rosh HaShanah one day
AlternativeDay = ConjunctionDay + 1
else:
AlternativeDay = ConjunctionDay
if (((AlternativeDay % 7) == 0) # If Rosh HaShanah would occur on Sunday,
or ((AlternativeDay % 7) == 3) # or Wednesday,
or ((AlternativeDay % 7) == 5)): # or Friday
# Then postpone it one (more) day
return (1+ AlternativeDay)
else:
return AlternativeDay
def DaysInHebrewYear(year):
"""Number of days in Hebrew year."""
return ((HebrewCalendarElapsedDays(year + 1)) -
(HebrewCalendarElapsedDays(year)))
def LongHeshvan(year):
"""True if Heshvan is long in Hebrew year."""
if ((DaysInHebrewYear(year) % 10) == 5):
return 1
else:
return 0
def ShortKislev(year):
"""True if Kislev is short in Hebrew year."""
if ((DaysInHebrewYear(year) % 10) == 3):
return 1
else:
return 0
def LastDayOfHebrewMonth(month, year):
"""Last day of month in Hebrew year."""
if ((month == 2)
or (month == 4)
or (month == 6)
or ((month == 8) and not (LongHeshvan(year)))
or ((month == 9) and ShortKislev(year))
or (month == 10)
or ((month == 12) and not (HebrewLeapYear(year)))
or (month == 13)):
return 29
else:
return 30
class HebrewDate(Date):
"""The Hebrew Calendar is one of the most complicated calendars. It
attempts to keep the months strictly lunar cycles and still be in sync
with a solar year. On top of this are strict guidelines as to what
days certain religious events must occur. It used to be the case that
a new month was decreed when a new moon was ``sighted''. For the most
part this has been turned over to astronomical calculations which are
more accurate. The new day begins at sunset. For our purposes we start
new days at 6pm local time. This is a good enough approximation to
when the sun sets.
"""
def __init__(self, d, m = 0, y = 0):
"""year # 1...
month # 1..LastMonthOfHebrewYear(year)
day # 1..LastDayOfHebrewMonth(month, year)
"""
if ((m != 0) and (y != 0)):
Date.__init__(self)
self.month = m
self.day = d
self.year = y
else:
Date.__init__(self, d)
# Computes the Hebrew date from the absolute date.
year = (d - HebrewEpoch) / 366 # Approximation from below.
# Search forward for year from the approximation.
while (d >= HebrewDate(1,7,year + 1).absolute()):
year = year + 1
# Search forward for month from either Tishri or Nisan.
if (d < HebrewDate(1, 1, year).absolute()):
month = 7 # Start at Tishri
else:
month = 1 # Start at Nisan
while (d > HebrewDate(LastDayOfHebrewMonth(month,year), month, year).absolute()):
month = month + 1
# Calculate the day by subtraction.
day = d - HebrewDate(1, month, year).absolute() + 1
self.year, self.month, self.day = year, month, day
def absolute(self):
"""Computes the absolute date of Hebrew date.
"Absolute date" means the number of days elapsed since the Gregorian date
Sunday, December 31, 1 BC. (Since there was no year 0, the year following
1 BC is 1 AD.) Thus the Gregorian date January 1, 1 AD is absolute date
number 1.
"""
if self.abs:
return self.abs
DayInYear = self.day # Days so far this month.
if (self.month < 7): # Before Tishri, so add days in prior months
# this year before and after Nisan.
m = 7
while (m <= (LastMonthOfHebrewYear(self.year))):
DayInYear = DayInYear + LastDayOfHebrewMonth(m, self.year)
m = m + 1
m = 1
while (m < self.month):
DayInYear = DayInYear + LastDayOfHebrewMonth(m, self.year)
m = m + 1
else:
# Add days in prior months this year
m = 7
while (m < self.month):
DayInYear = DayInYear + LastDayOfHebrewMonth(m, self.year)
m = m + 1
self.abs = (DayInYear +
(HebrewCalendarElapsedDays(self.year)# Days in prior years.
+ HebrewEpoch)) # Days elapsed before absolute date 1.
return self.abs
if __name__ == "__main__":
import string
while (1):
y = string.atoi(raw_input("Enter year (>0): "))
m = string.atoi(raw_input("Enter month (1..12): "))
d = string.atoi(raw_input("Enter day (1..%s ): " % LastDayOfGregorianMonth(m, y)))
g = GregorianDate(d,m,y)
a = g.absolute()
print "%s = %s = %s" % (g, a, DayName[g.absolute() % 7])
g2 = g + 1
a = g2.absolute()
print "%s + 1 = %s = %s" % (g, g2, a)
g = GregorianDate(a)
a = g.absolute()
print "= gregorian date %s = absolute date %s " % (g, a)
j = JulianDate(a)
a = j.absolute()
print "= julian date %s = absolute date %s" % (j, a)
i = IsoDate(a)
a = i.absolute()
print "= iso date %s = absolute date %s " % (i,a)
h = HebrewDate(a)
a = h.absolute()
print "= hebrew date %s = absolute date %s" % (h, a)
isl= IslamicDate(a)
a = isl.absolute()
print "= islamic date %s = absolute date %s" % (isl, a)
--
Christian Egli
Switching Test Solutions AG, Friesenbergstr. 75, CH-8055 Zuerich
_______________
DB-SIG - SIG on Tabular Databases in Python
send messages to: db-sig@python.org
administrivia to: db-sig-request@python.org
_______________