source: inundation/pyvolution/normalDate.py @ 1882

Last change on this file since 1882 was 1393, checked in by steve, 19 years ago

Added ghosts to rectangle

File size: 16.9 KB
Line 
1#!/usr/bin/env python
2# normalDate.py - version 1.3 - 2002/05/24
3# Author: Jeff Bauer, Rubicon Research - jbauer@rubic.com
4# License: Same as Python 2.1 or later
5
6import time
7from types import IntType, ListType, StringType, TupleType
8
9_bigBangScalar = -4345732  # based on (-9999, 1, 1) BC/BCE minimum
10_bigCrunchScalar = 2958463  # based on (9999,12,31) AD/CE maximum
11_daysInMonthNormal = [31,28,31,30,31,30,31,31,30,31,30,31]
12_daysInMonthLeapYear = [31,29,31,30,31,30,31,31,30,31,30,31]
13_dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
14                  'Friday', 'Saturday', 'Sunday']
15_monthName = ['January', 'February', 'March', 'April', 'May', 'June',
16              'July','August','September','October','November','December']
17
18class NormalDateException(Exception):
19    """Exception class for NormalDate"""
20    pass
21
22class NormalDate:
23    """
24    NormalDate is a specialized class to handle dates without
25    all the excess baggage (time zones, daylight savings, leap
26    seconds, etc.) of other date structures.  The minimalist
27    strategy greatly simplifies its implementation and use.
28
29    Internally, NormalDate is stored as an integer with values
30    in a discontinuous range of -99990101 to 99991231.  The
31    integer value is used principally for storage and to simplify
32    the user interface.  Internal calculations are performed by
33    a scalar based on Jan 1, 1900.
34
35    Valid NormalDate ranges include (-9999,1,1) B.C.E. through
36    (9999,12,31) C.E./A.D.
37
38    1.3 - Added weekOfYear() method submitted by Harri Pasanen.
39    1.2 - Bugfix for Python2.2, which changed localtime's type
40          to time.struct_time rather than a tuple.  Thanks to
41          Paul Weimer for reporting this problem.  Added some,
42          but not all, of Robin Becker's ideas he incorporated
43          into ReportLab's version of normalDate.
44    1.1 - Added exception in setNormalDate for bad integer;
45          range() returns a list of normalDates rather than
46          a list of integers.
47    1.0 - No changes, except the version number.  After 3 years of use
48            by various parties I think we can consider it stable.
49    0.8 - added Prof. Stephen Walton's suggestion for a range method
50            - module author resisted the temptation to use lambda <0.5 wink>
51    0.7 - added Dan Winkler's suggestions for __add__, __sub__ methods
52    0.6 - modifications suggested by Kevin Digweed to fix:
53            - dayOfWeek, dayOfWeekAbbrev, clone methods
54            - permit NormalDate to be a better behaved superclass
55    0.5 - minor tweaking
56    0.4 - added methods __cmp__, __hash__
57        - added Epoch variable, scoped to the module
58        - added setDay, setMonth, setYear methods
59    0.3 - minor touch-ups
60    0.2 - fixed bug for certain B.C.E leap years
61        - added Jim Fulton's suggestions for short alias class name =ND
62          and __getstate__, __setstate__ methods
63
64    Special thanks for ideas and suggestions: 
65        Jim Fulton
66        Kevin Digweed
67        Paul Weimer
68        Robin Becker
69        Roedy Green
70        Stephen Walton
71    """
72    def __init__(self, normalDate=None):
73        """
74        Accept 1 of 4 values to initialize a NormalDate:
75            1. None - creates a NormalDate for the current day
76            2. integer in yyyymmdd format
77            3. string in yyyymmdd format
78            4. tuple in (yyyy, mm, dd) - localtime/gmtime can also be used
79        """
80        if normalDate is None:
81            self.setNormalDate(time.localtime(time.time()))
82        else:
83            self.setNormalDate(normalDate)
84
85    def add(self, days):
86        """add days to date; use negative integers to subtract"""
87        if not type(days) is IntType:
88            raise NormalDateException( \
89                'add method parameter must be integer type')
90        self.normalize(self.scalar() + days)
91
92    def __add__(self, days):
93        """add integer to normalDate and return a new, calculated value"""
94        if not type(days) is IntType:
95            raise NormalDateException( \
96                '__add__ parameter must be integer type')
97        cloned = self.clone()
98        cloned.add(days)
99        return cloned
100
101    def clone(self):
102        """return a cloned instance of this normalDate"""
103        return self.__class__(self.normalDate)
104
105    def __cmp__(self, target):
106        if target is None: 
107            return 1
108        elif not hasattr(target, 'normalDate'):
109            return 1
110        else:
111            return cmp(self.normalDate, target.normalDate)
112
113    def day(self):
114        """return the day as integer 1-31"""
115        return int(repr(self.normalDate)[-2:])
116
117    def dayOfWeek(self):
118        """return integer representing day of week, Mon=0, Tue=1, etc."""
119        return apply(dayOfWeek, self.toTuple())
120
121    def dayOfWeekAbbrev(self):
122        """return day of week abbreviation for current date: Mon, Tue, etc."""
123        return _dayOfWeekName[self.dayOfWeek()][:3]
124
125    def dayOfWeekName(self):
126        """return day of week name for current date: Monday, Tuesday, etc."""
127        return _dayOfWeekName[self.dayOfWeek()]
128
129    def dayOfYear(self):
130        """day of year"""
131        if self.isLeapYear():
132            daysByMonth = _daysInMonthLeapYear
133        else:
134            daysByMonth = _daysInMonthNormal
135        priorMonthDays = 0
136        for m in xrange(self.month() - 1):
137            priorMonthDays = priorMonthDays + daysByMonth[m]
138        return self.day() + priorMonthDays
139
140    def daysBetweenDates(self, normalDate):
141        """
142        return value may be negative, since calculation is
143        self.scalar() - arg
144        """
145        if type(normalDate) is _NormalDateType:
146            return self.scalar() - normalDate.scalar()
147        else:
148            return self.scalar() - NormalDate(normalDate).scalar()
149
150    def daysInMonth(self, month):
151        """returns last day of the month as integer 28-31"""
152        if self.isLeapYear():
153            return _daysInMonthLeapYear[month - 1]
154        else:
155            return _daysInMonthNormal[month - 1]
156
157    def equals(self, target):
158        if type(target) is _NormalDateType:
159            if target is None:
160                return self.normalDate is None
161            else:
162                return self.normalDate == target.normalDate
163        else:
164            return 0
165
166    def endOfMonth(self):
167        """returns (cloned) last day of month"""
168        return self.__class__(self.__repr__()[-8:-2] + \
169                              str(self.lastDayOfMonth()))
170
171    def firstDayOfMonth(self):
172        """returns (cloned) first day of month"""
173        return self.__class__(self.__repr__()[-8:-2] + "01")
174
175    def formatUS(self):
176        """return date as string in common US format: MM/DD/YY"""
177        d = self.__repr__()
178        return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-6:-4])
179
180    def formatUSCentury(self):
181        """return date as string in 4-digit year US format: MM/DD/YYYY"""
182        d = self.__repr__()
183        return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-8:-4])
184
185    def __getstate__(self):
186        """minimize persistent storage requirements"""
187        return self.normalDate
188
189    def __hash__(self):
190        return hash(self.normalDate)
191
192    def __int__(self):
193        return self.normalDate
194
195    def isLeapYear(self):
196        """
197        determine if specified year is leap year, returning true (1) or
198        false (0)
199        """
200        return isLeapYear(self.year())
201
202    def _isValidNormalDate(self, normalDate):
203        """checks for date validity in [-]yyyymmdd format"""
204        if type(normalDate) is not IntType:
205            return 0
206        if len(repr(normalDate)) > 9:
207            return 0
208        if normalDate < 0:
209            dateStr = "%09d" % normalDate
210        else:
211            dateStr = "%08d" % normalDate
212        if len(dateStr) < 8:
213            return 0
214        elif len(dateStr) == 9:
215            if (dateStr[0] != '-' and dateStr[0] != '+'):
216                return 0
217        year = int(dateStr[:-4])
218        if year < -9999 or year > 9999 or year == 0:
219            return 0    # note: zero (0) is not a valid year
220        month = int(dateStr[-4:-2])
221        if month < 1 or month > 12:
222            return 0
223        if isLeapYear(year):
224            maxDay = _daysInMonthLeapYear[month - 1]
225        else:
226            maxDay = _daysInMonthNormal[month - 1]
227        day = int(dateStr[-2:])
228        if day < 1 or day > maxDay:
229            return 0
230        if year == 1582 and month == 10 and day > 4 and day < 15:
231            return 0  # special case of 10 days dropped: Oct 5-14, 1582
232        return 1
233
234    def lastDayOfMonth(self):
235        """returns last day of the month as integer 28-31"""
236        if self.isLeapYear():
237            return _daysInMonthLeapYear[self.month() - 1]
238        else:
239            return _daysInMonthNormal[self.month() - 1]
240
241    def localeFormat(self):
242        """override this method to use your preferred locale format"""
243        return self.formatUS()
244
245    def month(self):
246        """returns month as integer 1-12"""
247        return int(repr(self.normalDate)[-4:-2])
248
249    def monthAbbrev(self):
250        """returns month as a 3-character abbreviation, i.e. Jan, Feb, etc."""
251        return _monthName[self.month() - 1][:3]
252
253    def monthName(self):
254        """returns month name, i.e. January, February, etc."""
255        return _monthName[self.month() - 1]
256
257    def normalize(self, scalar):
258        """convert scalar to normalDate"""
259        if scalar < _bigBangScalar:
260            msg = "normalize(%d): scalar below minimum" % \
261                  _bigBangScalar
262            raise NormalDateException(msg)
263        if scalar > _bigCrunchScalar:
264            msg = "normalize(%d): scalar exceeds maximum" % \
265                  _bigCrunchScalar
266            raise NormalDateException(msg)
267        from math import floor
268        if scalar >= -115860:
269            year = 1600 + int(floor((scalar + 109573) / 365.2425))
270        elif scalar >= -693597:
271            year = 4 + int(floor((scalar + 692502) / 365.2425))
272        else:
273            year = -4 + int(floor((scalar + 695058) / 365.2425))
274        days = scalar - firstDayOfYear(year) + 1
275        if days <= 0:
276            year = year - 1
277            days = scalar - firstDayOfYear(year) + 1
278        daysInYear = 365
279        if isLeapYear(year):
280            daysInYear = daysInYear + 1
281        if days > daysInYear:
282            year = year + 1
283            days = scalar - firstDayOfYear(year) + 1
284        # add 10 days if between Oct 15, 1582 and Dec 31, 1582
285        if (scalar >= -115860 and scalar <= -115783):
286            days = days + 10
287        if isLeapYear(year):
288            daysByMonth = _daysInMonthLeapYear
289        else:
290            daysByMonth = _daysInMonthNormal
291        dc = 0; month = 12
292        for m in xrange(len(daysByMonth)):
293            dc = dc + daysByMonth[m]
294            if dc >= days:
295                month = m + 1
296                break
297        # add up the days in prior months
298        priorMonthDays = 0
299        for m in xrange(month - 1):
300            priorMonthDays = priorMonthDays + daysByMonth[m]
301        day = days - priorMonthDays
302        self.setNormalDate((year, month, day))
303
304        def __radd__(self,days):
305                """for completeness"""
306                return self.__add__(days)
307
308    def range(self, days):
309        """Return a range of normalDates as a list.  Parameter
310        may be an int or normalDate."""
311        if type(days) is not IntType:
312            days = days - self  # if not int, assume arg is normalDate type
313        r = []
314        for i in range(days):
315            r.append(self + i)
316        return r
317
318    def __repr__(self):
319        """print format: [-]yyyymmdd"""
320        # Note: When disassembling a NormalDate string, be sure to
321        # count from the right, i.e. epochMonth = int(`Epoch`[-4:-2]),
322        # or the slice won't work for dates B.C.
323        if self.normalDate < 0:
324            return "%09d" % self.normalDate
325        else:
326            return "%08d" % self.normalDate
327
328        def __rsub__(self, v):
329                if type(v) is IntType:
330                        return NormalDate(v) - self
331                else:
332                        return v.scalar() - self.scalar()
333
334    def scalar(self):
335        """days since baseline date: Jan 1, 1900"""
336        (year, month, day) = self.toTuple()
337        days = firstDayOfYear(year) + day - 1
338        if self.isLeapYear():
339            for m in xrange(month - 1):
340                days = days + _daysInMonthLeapYear[m]
341        else:
342            for m in xrange(month - 1):
343                days = days + _daysInMonthNormal[m]
344        if year == 1582:
345            if month > 10 or (month == 10 and day > 4):
346                days = days - 10
347        return days
348
349    def setDay(self, day):
350        """set the day of the month"""
351        maxDay = self.lastDayOfMonth()
352        if day < 1 or day > maxDay:
353            msg = "day is outside of range 1 to %d" % maxDay
354            raise NormalDateException(msg)
355        (y, m, d) = self.toTuple()
356        self.setNormalDate((y, m, day))
357
358    def setMonth(self, month):
359        """set the month [1-12]"""
360        if month < 1 or month > 12:
361            raise NormalDateException('month is outside range 1 to 12')
362        (y, m, d) = self.toTuple()
363        self.setNormalDate((y, month, d))
364
365    def setNormalDate(self, normalDate):
366        """
367        accepts date as scalar string/integer (yyyymmdd) or tuple
368        (year, month, day, ...)"""
369        _type = type(normalDate)
370        if _type is IntType:
371            self.normalDate = normalDate
372        elif _type is StringType:
373            try:
374                self.normalDate = int(normalDate)
375            except ValueError:
376                raise NormalDateException("Bad integer: '%s'" % normalDate)
377        elif _type in (TupleType, ListType) or _type is _TimeType:
378            self.normalDate = int("%04d%02d%02d" % normalDate[:3])
379        elif _type is _NormalDateType:
380            self.normalDate = normalDate.normalDate
381        if not self._isValidNormalDate(self.normalDate):
382            msg = "unable to setNormalDate(%s)" % `normalDate`
383            raise NormalDateException(msg)
384
385    def setYear(self, year):
386        if year == 0:
387            raise NormalDateException('cannot set year to zero')
388        elif year < -9999:
389            raise NormalDateException('year cannot be less than -9999')
390        elif year > 9999:
391            raise NormalDateException('year cannot be greater than 9999')
392        (y, m, d) = self.toTuple()
393        self.setNormalDate((year, m, d))
394
395    __setstate__ = setNormalDate
396
397    def __sub__(self, v):
398        if type(v) is IntType:
399            return self.__add__(-v)
400        return self.scalar() - v.scalar()
401
402    def toTuple(self):
403        """return date as (year, month, day) tuple"""
404        return (self.year(), self.month(), self.day())
405
406    def weekOfYear(self):
407        """returns the week of the year: 1-52"""
408        week = self.dayOfYear() / 7 + 1
409        if week == 53:
410            week = 1
411        return week
412
413    def year(self):
414        """return year in yyyy format, negative values indicate B.C."""
415        return int(repr(self.normalDate)[:-4])
416
417#################  Utility functions  #################
418
419def bigBang():
420    """return lower boundary as a NormalDate"""
421    return NormalDate((-9999, 1, 1))
422
423def bigCrunch():
424    """return upper boundary as a NormalDate"""
425    return NormalDate((9999, 12, 31))
426
427def dayOfWeek(y, m, d):
428    """return integer representing day of week, Mon=0, Tue=1, etc."""
429    if m == 1 or m == 2:
430        m = m + 12
431        y = y - 1
432    return (d + 2*m + 3*(m+1)/5 + y + y/4 - y/100 + y/400) % 7
433
434def firstDayOfYear(year):
435    """number of days to the first of the year, relative to Jan 1, 1900"""
436    if type(year) is not IntType:
437        msg = "firstDayOfYear() expected integer, got %s" % type(year)
438        raise NormalDateException(msg)
439    if year == 0:
440        raise NormalDateException('first day of year cannot be zero (0)')
441    elif year < 0:  # BCE calculation
442        firstDay = (year * 365) + int((year - 1) / 4) - 693596
443    else:           # CE calculation
444        leapAdjust = int((year + 3) / 4)
445        if year > 1600:
446            leapAdjust = leapAdjust - int((year + 99 - 1600) / 100) + \
447                         int((year + 399 - 1600) / 400)
448        firstDay = year * 365 + leapAdjust - 693963
449        if year > 1582:
450            firstDay = firstDay - 10
451    return firstDay
452
453def isLeapYear(year):
454    """determine if specified year is leap year, returns Python boolean"""
455    if year < 1600:
456        if year % 4:
457            return 0
458        else:
459            return 1
460    elif year % 4 != 0:
461        return 0
462    elif year % 100 != 0:
463        return 1
464    elif year % 400 != 0:
465        return 0
466    else:
467        return 1
468
469ND=NormalDate
470Epoch=bigBang()
471_NormalDateType = type(Epoch)
472_TimeType = type(time.localtime(time.time()))
473
474if __name__ == '__main__':
475    today = ND()
476    print "NormalDate test:"
477    print "  Today (%s) is: %s %s" % \
478          (today, today.dayOfWeekAbbrev(), today.localeFormat())
479    yesterday = today - 1
480    print "  Yesterday was: %s %s" % \
481          (yesterday.dayOfWeekAbbrev(), yesterday.localeFormat())
482    tomorrow = today + 1
483    print "  Tomorrow will be: %s %s" % \
484          (tomorrow.dayOfWeekAbbrev(), tomorrow.localeFormat())
485    print "  Days between tomorrow and yesterday: %d" % \
486          (tomorrow - yesterday)
Note: See TracBrowser for help on using the repository browser.