Code

Ran it through pychecker, made fixes
[roundup.git] / roundup / date.py
index 4bdf11125a19b23fab63dc34adf88ea956408ade..c170fb6b3ac3c61c5f2921ce8fc984cf47b572aa 100644 (file)
@@ -4,7 +4,7 @@
 # under the same terms as Python, so long as this copyright message and
 # disclaimer are retained in their original form.
 #
-# IN NO EVENT SHALL THE BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: date.py,v 1.9 2001-08-07 00:15:51 richard Exp $
+# $Id: date.py,v 1.20 2002-02-21 23:34:51 richard Exp $
+
+__doc__ = """
+Date, time and time interval handling.
+"""
 
 import time, re, calendar
+from i18n import _
 
 class Date:
     '''
@@ -71,8 +76,6 @@ class Date:
         >>> Date("14:25", -5)
         <Date 2000-06-25.19:25:00>
     '''
-    isDate = 1
-
     def __init__(self, spec='.', offset=0):
         """Construct a date given a specification and a time zone offset.
 
@@ -101,14 +104,52 @@ class Date:
             self.second, x, x, x = time.gmtime(calendar.timegm(t))
 
     def __add__(self, other):
-        """Add an interval to this date to produce another date."""
-        t = (self.year + other.sign * other.year,
-            self.month + other.sign * other.month,
-            self.day + other.sign * other.day,
-            self.hour + other.sign * other.hour,
-            self.minute + other.sign * other.minute,
-            self.second + other.sign * other.second, 0, 0, 0)
-        return Date(time.gmtime(calendar.timegm(t)))
+        """Add an interval to this date to produce another date.
+        """
+        # do the basic calc
+        sign = other.sign
+        year = self.year + sign * other.year
+        month = self.month + sign * other.month
+        day = self.day + sign * other.day
+        hour = self.hour + sign * other.hour
+        minute = self.minute + sign * other.minute
+        second = self.second + sign * other.second
+
+        # now cope with under- and over-flow
+        # first do the time
+        while (second < 0 or second > 59 or minute < 0 or minute > 59 or
+                hour < 0 or hour > 59):
+            if second < 0: minute -= 1; second += 60
+            elif second > 59: minute += 1; second -= 60
+            if minute < 0: hour -= 1; minute += 60
+            elif minute > 59: hour += 1; minute -= 60
+            if hour < 0: day -= 1; hour += 24
+            elif hour > 59: day += 1; hour -= 24
+
+        # fix up the month so we're within range
+        while month < 1 or month > 12:
+            if month < 1: year -= 1; month += 12
+            if month > 12: year += 1; month -= 12
+
+        # now do the days, now that we know what month we're in
+        mdays = calendar.mdays
+        if month == 2 and calendar.isleap(year): month_days = 29
+        else: month_days = mdays[month]
+        while month < 1 or month > 12 or day < 0 or day > month_days:
+            # now to day under/over
+            if day < 0: month -= 1; day += month_days
+            elif day > month_days: month += 1; day -= month_days
+
+            # possibly fix up the month so we're within range
+            while month < 1 or month > 12:
+                if month < 1: year -= 1; month += 12
+                if month > 12: year += 1; month -= 12
+
+            # re-figure the number of days for this month
+            if month == 2 and calendar.isleap(year): month_days = 29
+            else: month_days = mdays[month]
+
+        return Date((year, month, day, hour, minute, second, 0, 0, 0))
 
     # XXX deviates from spec to allow subtraction of dates as well
     def __sub__(self, other):
@@ -116,40 +157,40 @@ class Date:
              1. an interval from this date to produce another date.
              2. a date from this date to produce an interval.
         """
-        if other.isDate:
-            # TODO this code will fall over laughing if the dates cross
-            # leap years, phases of the moon, ....
-            a = calendar.timegm((self.year, self.month, self.day, self.hour,
-                self.minute, self.second, 0, 0, 0))
-            b = calendar.timegm((other.year, other.month, other.day, other.hour,
-                other.minute, other.second, 0, 0, 0))
-            diff = a - b
-            if diff < 0:
-                sign = -1
-                diff = -diff
-            else:
-                sign = 1
-            S = diff%60
-            M = (diff/60)%60
-            H = (diff/(60*60))%60
-            if H>1: S = 0
-            d = (diff/(24*60*60))%30
-            if d>1: H = S = M = 0
-            m = (diff/(30*24*60*60))%12
-            if m>1: H = S = M = 0
-            y = (diff/(365*24*60*60))
-            if y>1: d = H = S = M = 0
-            return Interval((y, m, d, H, M, S), sign=sign)
-        t = (self.year - other.sign * other.year,
-             self.month - other.sign * other.month,
-             self.day - other.sign * other.day,
-             self.hour - other.sign * other.hour,
-             self.minute - other.sign * other.minute,
-             self.second - other.sign * other.second, 0, 0, 0)
-        return Date(time.gmtime(calendar.timegm(t)))
+        if isinstance(other, Interval):
+            other = Interval(other.get_tuple(), sign=-other.sign)
+            return self.__add__(other)
+
+        assert isinstance(other, Date), 'May only subtract Dates or Intervals'
+
+        # TODO this code will fall over laughing if the dates cross
+        # leap years, phases of the moon, ....
+        a = calendar.timegm((self.year, self.month, self.day, self.hour,
+            self.minute, self.second, 0, 0, 0))
+        b = calendar.timegm((other.year, other.month, other.day,
+            other.hour, other.minute, other.second, 0, 0, 0))
+        diff = a - b
+        if diff < 0:
+            sign = 1
+            diff = -diff
+        else:
+            sign = -1
+        S = diff%60
+        M = (diff/60)%60
+        H = (diff/(60*60))%60
+        if H>1: S = 0
+        d = (diff/(24*60*60))%30
+        if d>1: H = S = M = 0
+        m = (diff/(30*24*60*60))%12
+        if m>1: H = S = M = 0
+        y = (diff/(365*24*60*60))
+        if y>1: d = H = S = M = 0
+        return Interval((y, m, d, H, M, S), sign=sign)
 
     def __cmp__(self, other):
         """Compare this date to another date."""
+        if other is None:
+            return 1
         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
             r = cmp(getattr(self, attr), getattr(other, attr))
             if r: return r
@@ -163,11 +204,13 @@ class Date:
     def pretty(self):
         ''' print up the date date using a pretty format...
         '''
-        return time.strftime('%e %B %Y', (self.year, self.month,
+        str = time.strftime('%d %B %Y', (self.year, self.month,
             self.day, self.hour, self.minute, self.second, 0, 0, 0))
+        if str[0] == '0': return ' ' + str[1:]
+        return str
 
     def set(self, spec, offset=0, date_re=re.compile(r'''
-              (((?P<y>\d\d\d\d)-)?((?P<m>\d\d)-(?P<d>\d\d))?)? # yyyy-mm-dd
+              (((?P<y>\d\d\d\d)-)?((?P<m>\d\d?)-(?P<d>\d\d?))?)? # yyyy-mm-dd
               (?P<n>\.)?                                       # .
               (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?    # hh:mm:ss
               (?P<o>.+)?                                       # offset
@@ -176,7 +219,8 @@ class Date:
         '''
         m = date_re.match(spec)
         if not m:
-            raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]'
+            raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]]'
+                '[offset]')
         info = m.groupdict()
 
         # get the current date/time using the offset
@@ -236,11 +280,18 @@ class Interval:
     Example usage:
         >>> Interval("  3w  1  d  2:00")
         <Interval 22d 2:00>
-        >>> Date(". + 2d") - Interval("3w")
+        >>> Date(". + 2d") + Interval("- 3w")
         <Date 2000-06-07.00:34:02>
-    '''
-    isInterval = 1
 
+    Intervals are added/subtracted in order of:
+       seconds, minutes, hours, years, months, days
+
+    Calculations involving monts (eg '+2m') have no effect on days - only
+    days (or over/underflow from hours/mins/secs) will do that, and
+    days-per-month and leap years are accounted for. Leap seconds are not.
+
+    TODO: more examples, showing the order of addition operation
+    '''
     def __init__(self, spec, sign=1):
         """Construct an interval given a specification."""
         if type(spec) == type(''):
@@ -252,6 +303,8 @@ class Interval:
 
     def __cmp__(self, other):
         """Compare this interval to another interval."""
+        if other is None:
+            return 1
         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
             r = cmp(getattr(self, attr), getattr(other, attr))
             if r: return r
@@ -282,7 +335,7 @@ class Interval:
             \s*
             ((?P<d>\d+\s*)d)?    # day
             \s*
-            (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?   # time
+            (((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
             \s*
             ''', re.VERBOSE)):
         ''' set the date to the value in spec
@@ -292,7 +345,8 @@ class Interval:
         self.sign = 1
         m = interval_re.match(spec)
         if not m:
-            raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]'
+            raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
+                '[#d] [[[H]H:MM]:SS]')
 
         info = m.groupdict()
         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
@@ -309,49 +363,52 @@ class Interval:
     def __repr__(self):
         return '<Interval %s>'%self.__str__()
 
-    def pretty(self, threshold=('d', 5)):
+    def pretty(self):
         ''' print up the date date using one of these nice formats..
-            < 1 minute
-            < 15 minutes
-            < 30 minutes
-            < 1 hour
-            < 12 hours
-            < 1 day
-            otherwise, return None (so a full date may be displayed)
         '''
         if self.year or self.month > 2:
             return None
-        if self.month:
+        elif self.month or self.day > 13:
             days = (self.month * 30) + self.day
             if days > 28:
-                return '%s months'%int(days/30)
+                if int(days/30) > 1:
+                    s = _('%(number)s months')%{'number': int(days/30)}
+                else:
+                    s = _('1 month')
             else:
-                return '%s weeks'%int(days/7)
-        if self.day > 7:
-            return '%s weeks'%self.day
-        if self.day > 1:
-            return '%s days'%self.day
-        if self.day == 1 or self.hour > 12:
-            return 'yesterday'
-        if self.hour > 1:
-            return '%s hours'%self.hour
-        if self.hour == 1:
+                s = _('%(number)s weeks')%{'number': int(days/7)}
+        elif self.day > 7:
+            s = _('1 week')
+        elif self.day > 1:
+            s = _('%(number)s days')%{'number': self.day}
+        elif self.day == 1 or self.hour > 12:
+            if self.sign > 0:
+                return _('tomorrow')
+            else:
+                return _('yesterday')
+        elif self.hour > 1:
+            s = _('%(number)s hours')%{'number': self.hour}
+        elif self.hour == 1:
             if self.minute < 15:
-                return 'an hour'
-            quart = self.minute/15
-            if quart == 2:
-                return '1 1/2 hours'
-            return '1 %s/4 hours'%quart
-        if self.minute < 1:
-            return 'just now'
-        if self.minute == 1:
-            return '1 minute'
-        if self.minute < 15:
-            return '%s minutes'%self.minute
-        quart = int(self.minute/15)
-        if quart == 2:
-            return '1/2 an hour'
-        return '%s/4 hour'%quart
+                s = _('an hour')
+            elif self.minute/15 == 2:
+                s = _('1 1/2 hours')
+            else:
+                s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
+        elif self.minute < 1:
+            if self.sign > 0:
+                return _('in a moment')
+            else:
+                return _('just now')
+        elif self.minute == 1:
+            s = _('1 minute')
+        elif self.minute < 15:
+            s = _('%(number)s minutes')%{'number': self.minute}
+        elif int(self.minute/15) == 2:
+            s = _('1/2 an hour')
+        else:
+            s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
+        return s
 
     def get_tuple(self):
         return (self.year, self.month, self.day, self.hour, self.minute,
@@ -380,6 +437,45 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.19  2002/02/21 23:11:45  richard
+#  . fixed some problems in date calculations (calendar.py doesn't handle over-
+#    and under-flow). Also, hour/minute/second intervals may now be more than
+#    99 each.
+#
+# Revision 1.18  2002/01/23 20:00:50  jhermann
+# %e is a UNIXism and not documented for Python
+#
+# Revision 1.17  2002/01/16 07:02:57  richard
+#  . lots of date/interval related changes:
+#    - more relaxed date format for input
+#
+# Revision 1.16  2002/01/08 11:56:24  richard
+# missed an import _
+#
+# Revision 1.15  2002/01/05 02:27:00  richard
+# I18N'ification
+#
+# Revision 1.14  2001/11/22 15:46:42  jhermann
+# Added module docstrings to all modules.
+#
+# Revision 1.13  2001/09/18 22:58:37  richard
+#
+# Added some more help to roundu-admin
+#
+# Revision 1.12  2001/08/17 03:08:11  richard
+# fixed prettification of intervals of 1 week
+#
+# Revision 1.11  2001/08/15 23:43:18  richard
+# Fixed some isFooTypes that I missed.
+# Refactored some code in the CGI code.
+#
+# Revision 1.10  2001/08/07 00:24:42  richard
+# stupid typo
+#
+# Revision 1.9  2001/08/07 00:15:51  richard
+# Added the copyright/license notice to (nearly) all files at request of
+# Bizar Software.
+#
 # Revision 1.8  2001/08/05 07:46:12  richard
 # Changed date.Date to use regular string formatting instead of strftime -
 # win32 seems to have problems with %T and no hour... or something...