diff options
| author | pinky <> | 2007-01-07 23:34:45 +0000 |
|---|---|---|
| committer | pinky <> | 2007-01-07 23:34:45 +0000 |
| commit | ac5f8444449ad5cae6a7ca5f4c7e313c9caa9fdb (patch) | |
| tree | 05b3ad3a3a03ae428da5e07a4b605e2fa2daee1f /bin/reportlab/lib/normalDate.py | |
| parent | 3af6ec6aa7cbb9c5ae2a99e8e21f02a771924916 (diff) | |
RL2
bzr revid: pinky-2e44d4c158101b4eb29c9fb7cee91c9aaabfed06
Diffstat (limited to 'bin/reportlab/lib/normalDate.py')
| -rw-r--r-- | bin/reportlab/lib/normalDate.py | 605 |
1 files changed, 605 insertions, 0 deletions
diff --git a/bin/reportlab/lib/normalDate.py b/bin/reportlab/lib/normalDate.py new file mode 100644 index 00000000000..efcea715013 --- /dev/null +++ b/bin/reportlab/lib/normalDate.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python +# normalDate.py - version 1.0 - 20000717 +#hacked by Robin Becker 10/Apr/2001 +#major changes include +# using Types instead of type(0) etc +# BusinessDate class +# __radd__, __rsub__ methods +# formatMS stuff + +# derived from an original version created +# by Jeff Bauer of Rubicon Research and used +# with his kind permission +__version__=''' $Id: normalDate.py 2742 2005-12-12 11:58:08Z oualid $ ''' + + + +_bigBangScalar = -4345732 # based on (-9999, 1, 1) BC/BCE minimum +_bigCrunchScalar = 2958463 # based on (9999,12,31) AD/CE maximum +_daysInMonthNormal = [31,28,31,30,31,30,31,31,30,31,30,31] +_daysInMonthLeapYear = [31,29,31,30,31,30,31,31,30,31,30,31] +_dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'] +_monthName = ['January', 'February', 'March', 'April', 'May', 'June', + 'July','August','September','October','November','December'] + +from types import IntType, StringType, ListType, TupleType +import string, re, time, datetime +if hasattr(time,'struct_time'): + _DateSeqTypes = (ListType,TupleType,time.struct_time) +else: + _DateSeqTypes = (ListType,TupleType) + +_fmtPat = re.compile('\\{(m{1,5}|yyyy|yy|d{1,4})\\}',re.MULTILINE|re.IGNORECASE) +_iso_re = re.compile(r'(\d\d\d\d|\d\d)-(\d\d)-(\d\d)') + +def getStdMonthNames(): + return map(string.lower,_monthName) + +def getStdShortMonthNames(): + return map(lambda x: x[:3],getStdMonthNames()) + +def getStdDayNames(): + return map(string.lower,_dayOfWeekName) + +def getStdShortDayNames(): + return map(lambda x: x[:3],getStdDayNames()) + +def isLeapYear(year): + """determine if specified year is leap year, returns Python boolean""" + if year < 1600: + if year % 4: + return 0 + else: + return 1 + elif year % 4 != 0: + return 0 + elif year % 100 != 0: + return 1 + elif year % 400 != 0: + return 0 + else: + return 1 + +class NormalDateException(Exception): + """Exception class for NormalDate""" + pass + +class NormalDate: + """ + NormalDate is a specialized class to handle dates without + all the excess baggage (time zones, daylight savings, leap + seconds, etc.) of other date structures. The minimalist + strategy greatly simplifies its implementation and use. + + Internally, NormalDate is stored as an integer with values + in a discontinuous range of -99990101 to 99991231. The + integer value is used principally for storage and to simplify + the user interface. Internal calculations are performed by + a scalar based on Jan 1, 1900. + + Valid NormalDate ranges include (-9999,1,1) B.C.E. through + (9999,12,31) C.E./A.D. + + 1.0 - No changes, except the version number. After 3 years of use + by various parties I think we can consider it stable. + 0.8 - added Prof. Stephen Walton's suggestion for a range method + - module author resisted the temptation to use lambda <0.5 wink> + 0.7 - added Dan Winkler's suggestions for __add__, __sub__ methods + 0.6 - modifications suggested by Kevin Digweed to fix: + - dayOfWeek, dayOfWeekAbbrev, clone methods + - permit NormalDate to be a better behaved superclass + 0.5 - minor tweaking + 0.4 - added methods __cmp__, __hash__ + - added Epoch variable, scoped to the module + - added setDay, setMonth, setYear methods + 0.3 - minor touch-ups + 0.2 - fixed bug for certain B.C.E leap years + - added Jim Fulton's suggestions for short alias class name =ND + and __getstate__, __setstate__ methods + + Special thanks: Roedy Green + """ + def __init__(self, normalDate=None): + """ + Accept 1 of 4 values to initialize a NormalDate: + 1. None - creates a NormalDate for the current day + 2. integer in yyyymmdd format + 3. string in yyyymmdd format + 4. tuple in (yyyy, mm, dd) - localtime/gmtime can also be used + """ + if normalDate is None: + self.setNormalDate(time.localtime(time.time())) + else: + self.setNormalDate(normalDate) + + def add(self, days): + """add days to date; use negative integers to subtract""" + if not type(days) is IntType: + raise NormalDateException( \ + 'add method parameter must be integer type') + self.normalize(self.scalar() + days) + + def __add__(self, days): + """add integer to normalDate and return a new, calculated value""" + if not type(days) is IntType: + raise NormalDateException( \ + '__add__ parameter must be integer type') + cloned = self.clone() + cloned.add(days) + return cloned + + def __radd__(self,days): + '''for completeness''' + return self.__add__(days) + + def clone(self): + """return a cloned instance of this normalDate""" + return self.__class__(self.normalDate) + + def __cmp__(self, target): + if target is None: + return 1 + elif not hasattr(target, 'normalDate'): + return 1 + else: + return cmp(self.normalDate, target.normalDate) + + def day(self): + """return the day as integer 1-31""" + return int(repr(self.normalDate)[-2:]) + + def dayOfWeek(self): + """return integer representing day of week, Mon=0, Tue=1, etc.""" + return apply(dayOfWeek, self.toTuple()) + + def dayOfWeekAbbrev(self): + """return day of week abbreviation for current date: Mon, Tue, etc.""" + return _dayOfWeekName[self.dayOfWeek()][:3] + + def dayOfWeekName(self): + """return day of week name for current date: Monday, Tuesday, etc.""" + return _dayOfWeekName[self.dayOfWeek()] + + def dayOfYear(self): + """day of year""" + if self.isLeapYear(): + daysByMonth = _daysInMonthLeapYear + else: + daysByMonth = _daysInMonthNormal + priorMonthDays = 0 + for m in xrange(self.month() - 1): + priorMonthDays = priorMonthDays + daysByMonth[m] + return self.day() + priorMonthDays + + def daysBetweenDates(self, normalDate): + """ + return value may be negative, since calculation is + self.scalar() - arg + """ + if type(normalDate) is _NDType: + return self.scalar() - normalDate.scalar() + else: + return self.scalar() - NormalDate(normalDate).scalar() + + def equals(self, target): + if type(target) is _NDType: + if target is None: + return self.normalDate is None + else: + return self.normalDate == target.normalDate + else: + return 0 + + def endOfMonth(self): + """returns (cloned) last day of month""" + return self.__class__(self.__repr__()[-8:-2]+str(self.lastDayOfMonth())) + + def firstDayOfMonth(self): + """returns (cloned) first day of month""" + return self.__class__(self.__repr__()[-8:-2]+"01") + + def formatUS(self): + """return date as string in common US format: MM/DD/YY""" + d = self.__repr__() + return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-6:-4]) + + def formatUSCentury(self): + """return date as string in 4-digit year US format: MM/DD/YYYY""" + d = self.__repr__() + return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-8:-4]) + + def _fmtM(self): + return str(self.month()) + + def _fmtMM(self): + return '%02d' % self.month() + + def _fmtMMM(self): + return self.monthAbbrev() + + def _fmtMMMM(self): + return self.monthName() + + def _fmtMMMMM(self): + return self.monthName()[0] + + def _fmtD(self): + return str(self.day()) + + def _fmtDD(self): + return '%02d' % self.day() + + def _fmtDDD(self): + return self.dayOfWeekAbbrev() + + def _fmtDDDD(self): + return self.dayOfWeekName() + + def _fmtYY(self): + return '%02d' % (self.year()%100) + + def _fmtYYYY(self): + return str(self.year()) + + def formatMS(self,fmt): + '''format like MS date using the notation + {YY} --> 2 digit year + {YYYY} --> 4 digit year + {M} --> month as digit + {MM} --> 2 digit month + {MMM} --> abbreviated month name + {MMMM} --> monthname + {MMMMM} --> first character of monthname + {D} --> day of month as digit + {DD} --> 2 digit day of month + {DDD} --> abrreviated weekday name + {DDDD} --> weekday name + ''' + r = fmt[:] + f = 0 + while 1: + m = _fmtPat.search(r,f) + if m: + y = getattr(self,'_fmt'+string.upper(m.group()[1:-1]))() + i, j = m.span() + r = (r[0:i] + y) + r[j:] + f = i + len(y) + else: + return r + + def __getstate__(self): + """minimize persistent storage requirements""" + return self.normalDate + + def __hash__(self): + return hash(self.normalDate) + + def __int__(self): + return self.normalDate + + def isLeapYear(self): + """ + determine if specified year is leap year, returning true (1) or + false (0) + """ + return isLeapYear(self.year()) + + def _isValidNormalDate(self, normalDate): + """checks for date validity in [-]yyyymmdd format""" + if type(normalDate) is not IntType: + return 0 + if len(repr(normalDate)) > 9: + return 0 + if normalDate < 0: + dateStr = "%09d" % normalDate + else: + dateStr = "%08d" % normalDate + if len(dateStr) < 8: + return 0 + elif len(dateStr) == 9: + if (dateStr[0] != '-' and dateStr[0] != '+'): + return 0 + year = int(dateStr[:-4]) + if year < -9999 or year > 9999 or year == 0: + return 0 # note: zero (0) is not a valid year + month = int(dateStr[-4:-2]) + if month < 1 or month > 12: + return 0 + if isLeapYear(year): + maxDay = _daysInMonthLeapYear[month - 1] + else: + maxDay = _daysInMonthNormal[month - 1] + day = int(dateStr[-2:]) + if day < 1 or day > maxDay: + return 0 + if year == 1582 and month == 10 and day > 4 and day < 15: + return 0 # special case of 10 days dropped: Oct 5-14, 1582 + return 1 + + def lastDayOfMonth(self): + """returns last day of the month as integer 28-31""" + if self.isLeapYear(): + return _daysInMonthLeapYear[self.month() - 1] + else: + return _daysInMonthNormal[self.month() - 1] + + def localeFormat(self): + """override this method to use your preferred locale format""" + return self.formatUS() + + def month(self): + """returns month as integer 1-12""" + return int(repr(self.normalDate)[-4:-2]) + + def monthAbbrev(self): + """returns month as a 3-character abbreviation, i.e. Jan, Feb, etc.""" + return _monthName[self.month() - 1][:3] + + def monthName(self): + """returns month name, i.e. January, February, etc.""" + return _monthName[self.month() - 1] + + def normalize(self, scalar): + """convert scalar to normalDate""" + if scalar < _bigBangScalar: + msg = "normalize(%d): scalar below minimum" % \ + _bigBangScalar + raise NormalDateException(msg) + if scalar > _bigCrunchScalar: + msg = "normalize(%d): scalar exceeds maximum" % \ + _bigCrunchScalar + raise NormalDateException(msg) + from math import floor + if scalar >= -115860: + year = 1600 + int(floor((scalar + 109573) / 365.2425)) + elif scalar >= -693597: + year = 4 + int(floor((scalar + 692502) / 365.2425)) + else: + year = -4 + int(floor((scalar + 695058) / 365.2425)) + days = scalar - firstDayOfYear(year) + 1 + if days <= 0: + year = year - 1 + days = scalar - firstDayOfYear(year) + 1 + daysInYear = 365 + if isLeapYear(year): + daysInYear = daysInYear + 1 + if days > daysInYear: + year = year + 1 + days = scalar - firstDayOfYear(year) + 1 + # add 10 days if between Oct 15, 1582 and Dec 31, 1582 + if (scalar >= -115860 and scalar <= -115783): + days = days + 10 + if isLeapYear(year): + daysByMonth = _daysInMonthLeapYear + else: + daysByMonth = _daysInMonthNormal + dc = 0; month = 12 + for m in xrange(len(daysByMonth)): + dc = dc + daysByMonth[m] + if dc >= days: + month = m + 1 + break + # add up the days in prior months + priorMonthDays = 0 + for m in xrange(month - 1): + priorMonthDays = priorMonthDays + daysByMonth[m] + day = days - priorMonthDays + self.setNormalDate((year, month, day)) + + def range(self, days): + """Return a range of normalDates as a list. Parameter + may be an int or normalDate.""" + if type(days) is not IntType: + days = days - self # if not int, assume arg is normalDate type + r = [] + for i in range(days): + r.append(self + i) + return r + + def __repr__(self): + """print format: [-]yyyymmdd""" + # Note: When disassembling a NormalDate string, be sure to + # count from the right, i.e. epochMonth = int(`Epoch`[-4:-2]), + # or the slice won't work for dates B.C. + if self.normalDate < 0: + return "%09d" % self.normalDate + else: + return "%08d" % self.normalDate + + def scalar(self): + """days since baseline date: Jan 1, 1900""" + (year, month, day) = self.toTuple() + days = firstDayOfYear(year) + day - 1 + if self.isLeapYear(): + for m in xrange(month - 1): + days = days + _daysInMonthLeapYear[m] + else: + for m in xrange(month - 1): + days = days + _daysInMonthNormal[m] + if year == 1582: + if month > 10 or (month == 10 and day > 4): + days = days - 10 + return days + + def setDay(self, day): + """set the day of the month""" + maxDay = self.lastDayOfMonth() + if day < 1 or day > maxDay: + msg = "day is outside of range 1 to %d" % maxDay + raise NormalDateException(msg) + (y, m, d) = self.toTuple() + self.setNormalDate((y, m, day)) + + def setMonth(self, month): + """set the month [1-12]""" + if month < 1 or month > 12: + raise NormalDateException('month is outside range 1 to 12') + (y, m, d) = self.toTuple() + self.setNormalDate((y, month, d)) + + def setNormalDate(self, normalDate): + """ + accepts date as scalar string/integer (yyyymmdd) or tuple + (year, month, day, ...)""" + tn=type(normalDate) + if tn is IntType: + self.normalDate = normalDate + elif tn is StringType: + try: + self.normalDate = int(normalDate) + except: + m = _iso_re.match(normalDate) + if m: + self.setNormalDate(m.group(1)+m.group(2)+m.group(3)) + else: + raise NormalDateException("unable to setNormalDate(%s)" % `normalDate`) + elif tn in _DateSeqTypes: + self.normalDate = int("%04d%02d%02d" % normalDate[:3]) + elif tn is _NDType: + self.normalDate = normalDate.normalDate + elif isinstance(normalDate,(datetime.datetime,datetime.date)): + self.normalDate = (normalDate.year*100+normalDate.month)*100+normalDate.day + if not self._isValidNormalDate(self.normalDate): + raise NormalDateException("unable to setNormalDate(%s)" % `normalDate`) + + def setYear(self, year): + if year == 0: + raise NormalDateException('cannot set year to zero') + elif year < -9999: + raise NormalDateException('year cannot be less than -9999') + elif year > 9999: + raise NormalDateException('year cannot be greater than 9999') + (y, m, d) = self.toTuple() + self.setNormalDate((year, m, d)) + + __setstate__ = setNormalDate + + def __sub__(self, v): + if type(v) is IntType: + return self.__add__(-v) + return self.scalar() - v.scalar() + + def __rsub__(self,v): + if type(v) is IntType: + return NormalDate(v) - self + else: + return v.scalar() - self.scalar() + + def toTuple(self): + """return date as (year, month, day) tuple""" + return (self.year(), self.month(), self.day()) + + def year(self): + """return year in yyyy format, negative values indicate B.C.""" + return int(repr(self.normalDate)[:-4]) + +################# Utility functions ################# + +def bigBang(): + """return lower boundary as a NormalDate""" + return NormalDate((-9999, 1, 1)) + +def bigCrunch(): + """return upper boundary as a NormalDate""" + return NormalDate((9999, 12, 31)) + +def dayOfWeek(y, m, d): + """return integer representing day of week, Mon=0, Tue=1, etc.""" + if m == 1 or m == 2: + m = m + 12 + y = y - 1 + return (d + 2*m + 3*(m+1)/5 + y + y/4 - y/100 + y/400) % 7 + +def firstDayOfYear(year): + """number of days to the first of the year, relative to Jan 1, 1900""" + if type(year) is not IntType: + msg = "firstDayOfYear() expected integer, got %s" % type(year) + raise NormalDateException(msg) + if year == 0: + raise NormalDateException('first day of year cannot be zero (0)') + elif year < 0: # BCE calculation + firstDay = (year * 365) + int((year - 1) / 4) - 693596 + else: # CE calculation + leapAdjust = int((year + 3) / 4) + if year > 1600: + leapAdjust = leapAdjust - int((year + 99 - 1600) / 100) + \ + int((year + 399 - 1600) / 400) + firstDay = year * 365 + leapAdjust - 693963 + if year > 1582: + firstDay = firstDay - 10 + return firstDay + +def FND(d): + '''convert to ND if required''' + return (type(d) is _NDType) and d or ND(d) + +Epoch=bigBang() +ND=NormalDate +_NDType = type(Epoch) +BDEpoch=ND(15821018) +BDEpochScalar = -115857 + +class BusinessDate(NormalDate): + """ + Specialised NormalDate + """ + def add(self, days): + """add days to date; use negative integers to subtract""" + if not type(days) is IntType: + raise NormalDateException('add method parameter must be integer type') + self.normalize(self.scalar() + days) + + def __add__(self, days): + """add integer to BusinessDate and return a new, calculated value""" + if not type(days) is IntType: + raise NormalDateException('__add__ parameter must be integer type') + cloned = self.clone() + cloned.add(days) + return cloned + + def __sub__(self, v): + return type(v) is IntType and self.__add__(-v) or self.scalar() - v.scalar() + + def asNormalDate(self): + return ND(self.normalDate) + + def daysBetweenDates(self, normalDate): + return self.asNormalDate.daysBetweenDates(normalDate) + + def _checkDOW(self): + if self.dayOfWeek()>4: raise NormalDateException("%s isn't a business day" % `self.normalDate`) + + def normalize(self, i): + i = int(i) + NormalDate.normalize(self,(i/5)*7+i%5+BDEpochScalar) + + def scalar(self): + d = self.asNormalDate() + i = d - BDEpoch #luckily BDEpoch is a Monday so we don't have a problem + #concerning the relative weekday + return 5*(i/7) + i%7 + + def setNormalDate(self, normalDate): + NormalDate.setNormalDate(self,normalDate) + self._checkDOW() + +if __name__ == '__main__': + today = NormalDate() + print "NormalDate test:" + print " Today (%s) is: %s %s" % (today, today.dayOfWeekAbbrev(), today.localeFormat()) + yesterday = today - 1 + print " Yesterday was: %s %s" % (yesterday.dayOfWeekAbbrev(), yesterday.localeFormat()) + tomorrow = today + 1 + print " Tomorrow will be: %s %s" % (tomorrow.dayOfWeekAbbrev(), tomorrow.localeFormat()) + print " Days between tomorrow and yesterday: %d" % (tomorrow - yesterday) + print today.formatMS('{d}/{m}/{yy}') + print today.formatMS('{dd}/{m}/{yy}') + print today.formatMS('{ddd} {d}/{m}/{yy}') + print today.formatMS('{dddd} {d}/{m}/{yy}') + print today.formatMS('{d}/{mm}/{yy}') + print today.formatMS('{d}/{mmm}/{yy}') + print today.formatMS('{d}/{mmmm}/{yy}') + print today.formatMS('{d}/{m}/{yyyy}') + b = BusinessDate('20010116') + print 'b=',b,'b.scalar()', b.scalar() |
