Code

allow negative items, helping construct forms
[roundup.git] / roundup / date.py
index a6430d7504b37bf75ba2d06efec49a29fe343cef..d4f5f36d8acae0e51fd8d29120e530678631d579 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: date.py,v 1.51 2003-03-22 22:43:21 richard Exp $
+# $Id: date.py,v 1.56 2003-11-04 12:35:47 anthonybaxter Exp $
 
 __doc__ = """
 Date, time and time interval handling.
@@ -24,6 +24,15 @@ Date, time and time interval handling.
 import time, re, calendar, types
 from i18n import _
 
+def _add_granularity(src, order, value = 1):
+    '''Increment first non-None value in src dictionary ordered by 'order'
+    parameter
+    '''
+    for gran in order:
+        if src[gran]:
+            src[gran] = int(src[gran]) + value
+            break
+
 class Date:
     '''
     As strings, date-and-time stamps are specified with the date in
@@ -50,6 +59,8 @@ class Date:
       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
       "14:25" means <Date yyyy-mm-dd.19:25:00>
       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
+      "2003" means <Date 2003-01-01.00:00:00>
+      "2003-06" means <Date 2003-06-01.00:00:00>
       "." means "right now"
 
     The Date class should understand simple date expressions of the form
@@ -80,7 +91,8 @@ class Date:
     minute, second) is the serialisation format returned by the serialise()
     method, and is accepted as an argument on instatiation.
     '''
-    def __init__(self, spec='.', offset=0):
+    
+    def __init__(self, spec='.', offset=0, add_granularity=0):
         """Construct a date given a specification and a time zone offset.
 
           'spec' is a full date or a partial form, with an optional
@@ -88,23 +100,26 @@ class Date:
         'offset' is the local time zone offset from GMT in hours.
         """
         if type(spec) == type(''):
-            self.set(spec, offset=offset)
+            self.set(spec, offset=offset, add_granularity=add_granularity)
         else:
             y,m,d,H,M,S,x,x,x = spec
             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
             self.year, self.month, self.day, self.hour, self.minute, \
                 self.second, x, x, x = time.gmtime(ts)
 
+    usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS][offset]'
     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<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
             (?P<n>\.)?                                     # .
             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?  # hh:mm:ss
             (?P<o>.+)?                                     # offset
             ''', re.VERBOSE), serialised_re=re.compile(r'''
             (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
-            ''', re.VERBOSE)):
+            ''', re.VERBOSE), add_granularity=0):
         ''' set the date to the value in spec
         '''
+
         m = serialised_re.match(spec)
         if m is not None:
             # we're serialised - easy!
@@ -115,21 +130,27 @@ class Date:
         # not serialised data, try usual format
         m = date_re.match(spec)
         if m is None:
-            raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
-                '[[h]h:mm[:ss]][offset]')
+            raise ValueError, _('Not a date spec: %s' % self.usagespec)
 
         info = m.groupdict()
 
+        if add_granularity:
+            _add_granularity(info, 'SMHdmyab')
+
         # get the current date as our default
         y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
 
-        # override year, month, day parts
-        if info['m'] is not None and info['d'] is not None:
-            m = int(info['m'])
-            d = int(info['d'])
+        if info['y'] is not None or info['a'] is not None:
             if info['y'] is not None:
                 y = int(info['y'])
-            # time defaults to 00:00:00 GMT - offset (local midnight)
+                m,d = (1,1)
+                if info['m'] is not None:
+                    m = int(info['m'])
+                    if info['d'] is not None:
+                        d = int(info['d'])
+            if info['a'] is not None:
+                m = int(info['a'])
+                d = int(info['b'])
             H = -offset
             M = S = 0
 
@@ -140,6 +161,9 @@ class Date:
             S = 0
             if info['S'] is not None: S = int(info['S'])
 
+        if add_granularity:
+            S = S - 1
+        
         # now handle the adjustment of hour
         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
         self.year, self.month, self.day, self.hour, self.minute, \
@@ -149,8 +173,7 @@ class Date:
             try:
                 self.applyInterval(Interval(info['o'], allowdate=0))
             except ValueError:
-                raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
-                    '[[h]h:mm[:ss]][offset]')
+                raise ValueError, _('Not a date spec: %s' % self.usagespec)
 
     def addInterval(self, interval):
         ''' Add the interval to this date, returning the date tuple
@@ -181,24 +204,31 @@ class Date:
             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:
+        def get_mdays(year,month):
+            if month == 2 and calendar.isleap(year): return 29
+            else: return calendar.mdays[month]
+            
+        while month < 1 or month > 12 or day < 0 or day > get_mdays(year,month):
             # now to day under/over
-            if day < 0: month -= 1; day += month_days
-            elif day > month_days: month += 1; day -= month_days
+            if day < 0: 
+                # When going backwards, decrement month, then increment days
+                month -= 1
+                day += get_mdays(year,month)
+            elif day > get_mdays(year,month): 
+                # When going forwards, decrement days, then increment month
+                day -= get_mdays(year,month)
+                month += 1
 
             # 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 < 1: year -= 1; month += 12 ; day += 31
                 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 (year, month, day, hour, minute, second, 0, 0, 0)
 
+    def differenceDate(self, other):
+        "Return the difference between this date and another date"
+
     def applyInterval(self, interval):
         ''' Apply the interval to this date
         '''
@@ -223,29 +253,29 @@ class Date:
 
         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, ....
+        return self.dateDelta(other)
+
+    def dateDelta(self, other):
+        """ Produce an Interval of the difference between this date
+            and another date. Only returns days:hours:minutes:seconds.
+        """
+        # Returning intervals larger than a day is almost
+        # impossible - months, years, weeks, are all so imprecise.
         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:
+        if diff > 0:
             sign = 1
-            diff = -diff
         else:
             sign = -1
+            diff = -diff
         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)
+        H = (diff/(60*60))%24
+        d = diff/(24*60*60)
+        return Interval((0, 0, d, H, M, S), sign=sign)
 
     def __cmp__(self, other):
         """Compare this date to another date."""
@@ -345,10 +375,10 @@ class Interval:
 
     TODO: more examples, showing the order of addition operation
     '''
-    def __init__(self, spec, sign=1, allowdate=1):
+    def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
         """Construct an interval given a specification."""
         if type(spec) == type(''):
-            self.set(spec, allowdate)
+            self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
         else:
             if len(spec) == 7:
                 self.sign, self.year, self.month, self.day, self.hour, \
@@ -372,7 +402,8 @@ class Interval:
                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
                )?''', re.VERBOSE), serialised_re=re.compile('''
             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
-            (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE)):
+            (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
+            add_granularity=0):
         ''' set the date to the value in spec
         '''
         self.year = self.month = self.week = self.day = self.hour = \
@@ -389,6 +420,9 @@ class Interval:
 
         # pull out all the info specified
         info = m.groupdict()
+        if add_granularity:
+            _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
+
         valid = 0
         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
                 'H':'hour', 'M':'minute', 'S':'second'}.items():
@@ -576,7 +610,7 @@ class Interval:
         if self.sign < 0: 
             s = s + _(' ago')
         else:
-            s = _('in') + s
+            s = _('in ') + s
         return s
 
     def get_tuple(self):
@@ -654,7 +688,7 @@ class Range:
         <Range from None to 2003-03-09.20:00:00>
 
     """
-    def __init__(self, spec, Type, **params):
+    def __init__(self, spec, Type, allow_granularity=1, **params):
         """Initializes Range of type <Type> from given <spec> string.
         
         Sets two properties - from_value and to_value. None assigned to any of
@@ -666,8 +700,8 @@ class Range:
         
         """
         self.range_type = Type
-        re_range = r'(?:^|(?:from)?(.+?))(?:to(.+?)$|$)'
-        re_geek_range = r'(?:^|(.+?))(?:;(.+?)$|$)'
+        re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
+        re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
         # Check which syntax to use
         if  spec.find(';') == -1:
             # Native english
@@ -682,7 +716,11 @@ class Range:
             if self.to_value:
                 self.to_value = Type(self.to_value.strip(), **params)
         else:
-            raise ValueError, "Invalid range"
+            if allow_granularity:
+                self.from_value = Type(spec, **params)
+                self.to_value = Type(spec, add_granularity=1, **params)
+            else:
+                raise ValueError, "Invalid range"
 
     def __str__(self):
         return "from %s to %s" % (self.from_value, self.to_value)
@@ -691,12 +729,17 @@ class Range:
         return "<Range %s>" % self.__str__()
  
 def test_range():
-    rspecs = ("from 2-12 to 4-2", "18:00 TO +2m", "12:00", "tO +3d",
-        "2002-11-10; 2002-12-12", "; 20:00 +1d")
+    rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
+        "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
+    rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
     for rspec in rspecs:
         print '>>> Range("%s")' % rspec
         print `Range(rspec, Date)`
         print
+    for rspec in rispecs:
+        print '>>> Range("%s")' % rspec
+        print `Range(rspec, Interval)`
+        print
 
 def test():
     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
@@ -705,7 +748,7 @@ def test():
         print `Interval(interval)`
 
     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
-        "08-13.22:13", "14:25")
+        "08-13.22:13", "14:25", '2002-12')
     for date in dates:
         print '>>> Date("%s")'%date
         print `Date(date)`
@@ -716,6 +759,6 @@ def test():
         print `Date(date) + Interval(interval)`
 
 if __name__ == '__main__':
-    test_range()
+    test()
 
 # vim: set filetype=python ts=4 sw=4 et si