home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyth_os2.zip / python-1.0.2 / Demo / classes / Dates.py < prev    next >
Text File  |  1993-09-21  |  8KB  |  234 lines

  1. # Class Date supplies date objects that support date arithmetic.
  2. #
  3. # Date(month,day,year) returns a Date object.  An instance prints as,
  4. # e.g., 'Mon 16 Aug 1993'.
  5. #
  6. # Addition, subtraction, comparison operators, min, max, and sorting
  7. # all work as expected for date objects:  int+date or date+int returns
  8. # the date `int' days from `date'; date+date raises an exception;
  9. # date-int returns the date `int' days before `date'; date2-date1 returns
  10. # an integer, the number of days from date1 to date2; int-date raises an
  11. # exception; date1 < date2 is true iff date1 occurs before date2 (&
  12. # similarly for other comparisons); min(date1,date2) is the earlier of
  13. # the two dates and max(date1,date2) the later; and date objects can be
  14. # used as dictionary keys.
  15. #
  16. # Date objects support one visible method, date.weekday().  This returns
  17. # the day of the week the date falls on, as a string.
  18. #
  19. # Date objects also have 4 (conceptually) read-only data attributes:
  20. #   .month  in 1..12
  21. #   .day    in 1..31
  22. #   .year   int or long int
  23. #   .ord    the ordinal of the date relative to an arbitrary staring point
  24. #
  25. # The Dates module also supplies function today(), which returns the
  26. # current date as a date object.
  27. #
  28. # Those entranced by calendar trivia will be disappointed, as no attempt
  29. # has been made to accommodate the Julian (etc) system.  On the other
  30. # hand, at least this package knows that 2000 is a leap year but 2100
  31. # isn't, and works fine for years with a hundred decimal digits <wink>.
  32.  
  33. # Tim Peters   tim@ksr.com
  34. # not speaking for Kendall Square Research Corp
  35.  
  36. _MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May',
  37.          'June', 'July', 'August', 'September', 'October',
  38.          'November', 'December' ]
  39.  
  40. _DAY_NAMES = [ 'Friday', 'Saturday', 'Sunday', 'Monday',
  41.            'Tuesday', 'Wednesday', 'Thursday' ]
  42.  
  43. _DAYS_IN_MONTH = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
  44.  
  45. _DAYS_BEFORE_MONTH = []
  46. dbm = 0
  47. for dim in _DAYS_IN_MONTH:
  48.     _DAYS_BEFORE_MONTH.append(dbm)
  49.     dbm = dbm + dim
  50. del dbm, dim
  51.  
  52. _INT_TYPES = type(1), type(1L)
  53.  
  54. def _is_leap( year ):        # 1 if leap year, else 0
  55.     if year % 4 != 0: return 0
  56.     if year % 400 == 0: return 1
  57.     return year % 100 != 0
  58.  
  59. def _days_in_year( year ):    # number of days in year
  60.     return 365 + _is_leap(year)
  61.  
  62. def _days_before_year( year ):    # number of days before year
  63.     return year*365L + (year+3)/4 - (year+99)/100 + (year+399)/400
  64.  
  65. def _days_in_month( month, year ):    # number of days in month of year
  66.     if month == 2 and _is_leap(year): return 29
  67.     return _DAYS_IN_MONTH[month-1]
  68.  
  69. def _days_before_month( month, year ):    # number of days in year before month
  70.     return _DAYS_BEFORE_MONTH[month-1] + (month > 2 and _is_leap(year))
  71.  
  72. def _date2num( date ):        # compute ordinal of date.month,day,year
  73.     return _days_before_year( date.year ) + \
  74.        _days_before_month( date.month, date.year ) + \
  75.        date.day
  76.  
  77. _DI400Y = _days_before_year( 400 )    # number of days in 400 years
  78.  
  79. def _num2date( n ):        # return date with ordinal n
  80.     if type(n) not in _INT_TYPES:
  81.     raise TypeError, 'argument must be integer: ' + `type(n)`
  82.  
  83.     ans = Date(1,1,1)    # arguments irrelevant; just getting a Date obj
  84.     ans.ord = n
  85.  
  86.     n400 = (n-1)/_DI400Y        # # of 400-year blocks preceding
  87.     year, n = 400 * n400, n - _DI400Y * n400
  88.     more = n / 365
  89.     dby = _days_before_year( more )
  90.     if dby >= n:
  91.     more = more - 1
  92.     dby = dby - _days_in_year( more )
  93.     year, n = year + more, int(n - dby)
  94.  
  95.     try: year = int(year)        # chop to int, if it fits
  96.     except ValueError: pass
  97.  
  98.     month = min( n/29 + 1, 12 )
  99.     dbm = _days_before_month( month, year )
  100.     if dbm >= n:
  101.     month = month - 1
  102.     dbm = dbm - _days_in_month( month, year )
  103.  
  104.     ans.month, ans.day, ans.year = month, n-dbm, year
  105.     return ans
  106.  
  107. def _num2day( n ):    # return weekday name of day with ordinal n
  108.     return _DAY_NAMES[ int(n % 7) ]
  109.  
  110.  
  111. class Date:
  112.     def __init__( self, month, day, year ):
  113.     if not 1 <= month <= 12:
  114.         raise ValueError, 'month must be in 1..12: ' + `month`
  115.     dim = _days_in_month( month, year )
  116.     if not 1 <= day <= dim:
  117.         raise ValueError, 'day must be in 1..' + `dim` + ': ' + `day`
  118.     self.month, self.day, self.year = month, day, year
  119.     self.ord = _date2num( self )
  120.  
  121.     def __cmp__( self, other ):
  122.     return cmp( self.ord, other.ord )
  123.  
  124.     # define a hash function so dates can be used as dictionary keys
  125.     def __hash__( self ):
  126.     return hash( self.ord )
  127.  
  128.     # print as, e.g., Mon 16 Aug 1993
  129.     def __repr__( self ):
  130.     return '%.3s %2d %.3s ' % (
  131.           self.weekday(),
  132.           self.day,
  133.           _MONTH_NAMES[self.month-1] ) + `self.year`
  134.  
  135.     # automatic coercion is a pain for date arithmetic, since e.g.
  136.     # date-date and date-int mean different things.  So, in order to
  137.     # sneak integers past Python's coercion rules without losing the info
  138.     # that they're really integers (& not dates!), integers are disguised
  139.     # as instances of the derived class _DisguisedInt.  That this works
  140.     # relies on undocumented behavior of Python's coercion rules.
  141.     def __coerce__( self, other ):
  142.     if type(other) in _INT_TYPES:
  143.         return self, _DisguisedInt(other)
  144.     # if another Date, fine
  145.     if type(other) is type(self) and other.__class__ is Date:
  146.         return self, other
  147.  
  148.     # Python coerces int+date, but not date+int; in the former case,
  149.     # _DisguisedInt.__add__ handles it, so we only need to do
  150.     # date+int here
  151.     def __add__( self, n ):
  152.     if type(n) not in _INT_TYPES:
  153.         raise TypeError, 'can\'t add ' + `type(n)` + ' to date'
  154.     return _num2date( self.ord + n )
  155.  
  156.     # Python coerces all of int-date, date-int and date-date; the first
  157.     # case winds up in _DisguisedInt.__sub__, leaving the latter two
  158.     # for us
  159.     def __sub__( self, other ):
  160.     if other.__class__ is _DisguisedInt:    # date-int
  161.         return _num2date( self.ord - other.ord )
  162.     else:
  163.         return self.ord - other.ord        # date-date
  164.  
  165.     def weekday( self ):
  166.     return _num2day( self.ord )
  167.  
  168. # see comments before Date.__add__
  169. class _DisguisedInt( Date ):
  170.     def __init__( self, n ):
  171.     self.ord = n
  172.  
  173.     # handle int+date
  174.     def __add__( self, other ):
  175.     return other.__add__( self.ord )
  176.  
  177.     # complain about int-date
  178.     def __sub__( self, other ):
  179.     raise TypeError, 'Can\'t subtract date from integer'
  180.  
  181. def today():
  182.     import time
  183.     local = time.localtime(time.time())
  184.     return Date( local[1], local[2], local[0] )
  185.  
  186. DateTestError = 'DateTestError'
  187. def test( firstyear, lastyear ):
  188.     a = Date(9,30,1913)
  189.     b = Date(9,30,1914)
  190.     if `a` != 'Tue 30 Sep 1913':
  191.     raise DateTestError, '__repr__ failure'
  192.     if (not a < b) or a == b or a > b or b != b or \
  193.       a != 698982 or 698982 != a or \
  194.       (not a > 5) or (not 5 < a):
  195.     raise DateTestError, '__cmp__ failure'
  196.     if a+365 != b or 365+a != b:
  197.     raise DateTestError, '__add__ failure'
  198.     if b-a != 365 or b-365 != a:
  199.     raise DateTestError, '__sub__ failure'
  200.     try:
  201.     x = 1 - a
  202.     raise DateTestError, 'int-date should have failed'
  203.     except TypeError:
  204.     pass
  205.     try:
  206.     x = a + b
  207.     raise DateTestError, 'date+date should have failed'
  208.     except TypeError:
  209.     pass
  210.     if a.weekday() != 'Tuesday':
  211.     raise DateTestError, 'weekday() failure'
  212.     if max(a,b) is not b or min(a,b) is not a:
  213.     raise DateTestError, 'min/max failure'
  214.     d = {a-1:b, b:a+1}
  215.     if d[b-366] != b or d[a+(b-a)] != Date(10,1,1913):
  216.     raise DateTestError, 'dictionary failure'
  217.  
  218.     # verify date<->number conversions for first and last days for
  219.     # all years in firstyear .. lastyear
  220.  
  221.     lord = _days_before_year( firstyear )
  222.     y = firstyear
  223.     while y <= lastyear:
  224.     ford = lord + 1
  225.     lord = ford + _days_in_year(y) - 1
  226.     fd, ld = Date(1,1,y), Date(12,31,y)
  227.     if (fd.ord,ld.ord) != (ford,lord):
  228.         raise DateTestError, ('date->num failed', y)
  229.     fd, ld = _num2date(ford), _num2date(lord)
  230.     if (1,1,y,12,31,y) != \
  231.        (fd.month,fd.day,fd.year,ld.month,ld.day,ld.year):
  232.         raise DateTestError, ('num->date failed', y)
  233.     y = y + 1
  234.