Code

fixed Interval maths (sf bug 665357)
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Thu, 6 Mar 2003 02:33:57 +0000 (02:33 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Thu, 6 Mar 2003 02:33:57 +0000 (02:33 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1564 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
roundup/date.py
test/test_dates.py

index 7aaf1d31f57e6f780ca8aff7659f304619404ae8..06a234b5bb7bba4fe139137db8858cb62320d5ef 100644 (file)
@@ -31,6 +31,7 @@ Fixed:
 - re-worked detectors initialisation - woohoo, no more cross-importing!
 - fixed export/import of retired nodes (sf bug 685273)
 - fixed mutation of properties bug in RDBMS backends
+- fixed Interval maths (sf bug 665357)
 
 Feature:
 - support setting of properties on message and file through web and
index a5bb8a922796567b9572c9d939aaf9032cc5085d..04bcbda06524fc573a80c6cfe15938d830eafbc0 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: date.py,v 1.43 2003-02-23 19:05:14 kedder Exp $
+# $Id: date.py,v 1.44 2003-03-06 02:33:56 richard Exp $
 
 __doc__ = """
 Date, time and time interval handling.
 """
 
-import time, re, calendar
+import time, re, calendar, types
 from i18n import _
 
 class Date:
@@ -306,14 +306,28 @@ class Interval:
 
     Example usage:
         >>> Interval("  3w  1  d  2:00")
-        <Interval 22d 2:00>
+        <Interval 22d 2:00>
         >>> Date(". + 2d") + Interval("- 3w")
         <Date 2000-06-07.00:34:02>
-
-    Intervals are added/subtracted in order of:
+        >>> Interval('1:59:59') + Interval('00:00:01')
+        <Interval + 2:00>
+        >>> Interval('2:00') + Interval('- 00:00:01')
+        <Interval + 1:59:59>
+        >>> Interval('1y')/2
+        <Interval + 6m>
+        >>> Interval('1:00')/2
+        <Interval + 0:30>
+
+    Interval arithmetic is handled in a couple of special ways, trying
+    to cater for the most common cases. Fundamentally, Intervals which
+    have both date and time parts will result in strange results in
+    arithmetic - because of the impossibility of handling day->month->year
+    over- and under-flows. Intervals may also be divided by some number.
+
+    Intervals are added to Dates in order of:
        seconds, minutes, hours, years, months, days
 
-    Calculations involving monts (eg '+2m') have no effect on days - only
+    Calculations involving months (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.
 
@@ -350,8 +364,7 @@ class Interval:
 
     def __str__(self):
         """Return this interval as a string."""
-        sign = {1:'+', -1:'-'}[self.sign]
-        l = [sign]
+        l = []
         if self.year: l.append('%sy'%self.year)
         if self.month: l.append('%sm'%self.month)
         if self.day: l.append('%sd'%self.day)
@@ -359,6 +372,8 @@ class Interval:
             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
         elif self.hour or self.minute:
             l.append('%d:%02d'%(self.hour, self.minute))
+        if l:
+            l.insert(0, {1:'+', -1:'-'}[self.sign])
         return ' '.join(l)
 
     def __add__(self, other):
@@ -368,15 +383,78 @@ class Interval:
         elif isinstance(other, Interval):
             # add the other Interval to this one
             a = self.get_tuple()
+            as = a[0]
             b = other.get_tuple()
-            if b[0] < 0:
-                i = Interval([x-y for x,y in zip(a[1:],b[1:])])
-            else:
-                i = Interval([x+y for x,y in zip(a[1:],b[1:])])
-            return i
+            bs = b[0]
+            i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
+            i.insert(0, 1)
+            i = fixTimeOverflow(i)
+            return Interval(i)
+        # nope, no idea what to do with this other...
+        raise TypeError, "Can't add %r"%other
+
+    def __sub__(self, other):
+        if isinstance(other, Date):
+            # the other is a Date - produce a Date
+            interval = Interval(self.get_tuple())
+            interval.sign *= -1
+            return Date(other.addInterval(interval))
+        elif isinstance(other, Interval):
+            # add the other Interval to this one
+            a = self.get_tuple()
+            as = a[0]
+            b = other.get_tuple()
+            bs = b[0]
+            i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
+            i.insert(0, 1)
+            i = fixTimeOverflow(i)
+            return Interval(i)
         # nope, no idea what to do with this other...
         raise TypeError, "Can't add %r"%other
 
+    def __div__(self, other):
+        ''' Divide this interval by an int value.
+
+            Can't divide years and months sensibly in the _same_
+            calculation as days/time, so raise an error in that situation.
+        '''
+        try:
+            other = float(other)
+        except TypeError:
+            raise ValueError, "Can only divide Intervals by numbers"
+
+        y, m, d, H, M, S = (self.year, self.month, self.day,
+            self.hour, self.minute, self.second)
+        if y or m:
+            if d or H or M or S:
+                raise ValueError, "Can't divide Interval with date and time"
+            months = self.year*12 + self.month
+            months *= self.sign
+
+            months = int(months/other)
+
+            sign = months<0 and -1 or 1
+            m = months%12
+            y = months / 12
+            return Interval((sign, y, m, 0, 0, 0, 0))
+
+        else:
+            # handle a day/time division
+            seconds = S + M*60 + H*60*60 + d*60*60*24
+            seconds *= self.sign
+
+            seconds = int(seconds/other)
+
+            sign = seconds<0 and -1 or 1
+            seconds *= sign
+            S = seconds%60
+            seconds /= 60
+            M = seconds%60
+            seconds /= 60
+            H = seconds%24
+            d = seconds / 24
+            return Interval((sign, 0, 0, d, H, M, S))
+
     def set(self, spec, interval_re=re.compile('''
             \s*(?P<s>[-+])?         # + or -
             \s*((?P<y>\d+\s*)y)?    # year
@@ -477,6 +555,38 @@ class Interval:
         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
             self.day, self.hour, self.minute, self.second)
 
+def fixTimeOverflow(time):
+    ''' Handle the overflow in the time portion (H, M, S) of "time":
+            (sign, y,m,d,H,M,S)
+
+        Overflow and underflow will at most affect the _days_ portion of
+        the date. We do not overflow days to months as we don't know _how_
+        to, generally.
+    '''
+    # XXX we could conceivably use this function for handling regular dates
+    # XXX too - we just need to interrogate the month/year for the day
+    # XXX overflow...
+
+    sign, y, m, d, H, M, S = time
+    seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
+    if seconds:
+        sign = seconds<0 and -1 or 1
+        seconds *= sign
+        S = seconds%60
+        seconds /= 60
+        M = seconds%60
+        seconds /= 60
+        H = seconds%24
+        d = seconds / 24
+    else:
+        months = y*12 + m
+        sign = months<0 and -1 or 1
+        months *= sign
+        m = months%12
+        y = months/12
+
+    return (sign, y, m, d, H, M, S)
+
 
 def test():
     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
index b16a8ca73a0d086a933f03966cba67e261f0a4d7..f88212a72774db5bc0bf5253b8c19d501de09cbf 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_dates.py,v 1.17 2003-02-24 15:38:51 kedder Exp $ 
+# $Id: test_dates.py,v 1.18 2003-03-06 02:33:57 richard Exp $ 
 
 import unittest, time
 
-from roundup.date import Date, Interval
+from roundup.date import Date, Interval, fixTimeOverflow
 
 class DateTestCase(unittest.TestCase):
     def testDateInterval(self):
@@ -71,7 +71,9 @@ class DateTestCase(unittest.TestCase):
         date = Date("8:47:11", -5)
         ae(str(date), '%s-%02d-%02d.13:47:11'%(y, m, d))
 
-        # now check calculations
+    def testOffsetRandom(self):
+        ae = self.assertEqual
+        # XXX unsure of the usefulness of these, they're pretty random
         date = Date('2000-01-01') + Interval('- 2y 2m')
         ae(str(date), '1997-11-01.00:00:00')
         date = Date('2000-01-01 - 2y 2m')
@@ -86,7 +88,8 @@ class DateTestCase(unittest.TestCase):
         date = Date('2001-01-01') + Interval('60d')
         ae(str(date), '2001-03-02.00:00:00')
 
-        # time additions
+    def testOffsetAdd(self):
+        ae = self.assertEqual
         date = Date('2000-02-28.23:59:59') + Interval('00:00:01')
         ae(str(date), '2000-02-29.00:00:00')
         date = Date('2001-02-28.23:59:59') + Interval('00:00:01')
@@ -107,7 +110,8 @@ class DateTestCase(unittest.TestCase):
         date = Date('2001-02-28.22:58:59') + Interval('00:00:3661')
         ae(str(date), '2001-03-01.00:00:00')
 
-        # now subtractions
+    def testOffsetSub(self):
+        ae = self.assertEqual
         date = Date('2000-01-01') - Interval('- 2y 2m')
         ae(str(date), '2002-03-01.00:00:00')
         date = Date('2000-01-01') - Interval('2m')
@@ -138,12 +142,14 @@ class DateTestCase(unittest.TestCase):
         date = Date('2001-03-01.00:00:00') - Interval('00:00:3661')
         ae(str(date), '2001-02-28.22:58:59')
 
-        # local()
+    def testDateLocal(self):
+        ae = self.assertEqual
         date = Date("02:42:20")
         date = date.local(10)
+        y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
         ae(str(date), '%s-%02d-%02d.12:42:20'%(y, m, d))
 
-    def testInterval(self):
+    def testIntervalInit(self):
         ae = self.assertEqual
         ae(str(Interval('3y')), '+ 3y')
         ae(str(Interval('2 y 1 m')), '+ 2y 1m')
@@ -153,16 +159,56 @@ class DateTestCase(unittest.TestCase):
         ae(str(Interval(' 14:00 ')), '+ 14:00')
         ae(str(Interval(' 0:04:33 ')), '+ 0:04:33')
 
-        # __add__
-        # XXX these are fairly arbitrary and need fixing once the __add__
-        # code handles the odd cases more correctly
+    def testIntervalAdd(self):
+        ae = self.assertEqual
         ae(str(Interval('1y') + Interval('1y')), '+ 2y')
         ae(str(Interval('1y') + Interval('1m')), '+ 1y 1m')
         ae(str(Interval('1y') + Interval('2:40')), '+ 1y 2:40')
-        ae(str(Interval('1y') + Interval('- 1y')), '+')
-        ae(str(Interval('1y') + Interval('- 1m')), '+ 1y -1m')
-
-# TODO test add, subtraction, ?division?
+        ae(str(Interval('1y') + Interval('- 1y')), '')
+        ae(str(Interval('- 1y') + Interval('1y')), '')
+        ae(str(Interval('- 1y') + Interval('- 1y')), '- 2y')
+        ae(str(Interval('1y') + Interval('- 1m')), '+ 11m')
+        ae(str(Interval('1:00') + Interval('1:00')), '+ 2:00')
+        ae(str(Interval('0:50') + Interval('0:50')), '+ 1:40')
+        ae(str(Interval('1:50') + Interval('- 1:50')), '')
+        ae(str(Interval('- 1:50') + Interval('1:50')), '')
+        ae(str(Interval('- 1:50') + Interval('- 1:50')), '- 3:40')
+        ae(str(Interval('1:59:59') + Interval('00:00:01')), '+ 2:00')
+        ae(str(Interval('2:00') + Interval('- 00:00:01')), '+ 1:59:59')
+
+    def testIntervalSub(self):
+        ae = self.assertEqual
+        ae(str(Interval('1y') - Interval('- 1y')), '+ 2y')
+        ae(str(Interval('1y') - Interval('- 1m')), '+ 1y 1m')
+        ae(str(Interval('1y') - Interval('- 2:40')), '+ 1y 2:40')
+        ae(str(Interval('1y') - Interval('1y')), '')
+        ae(str(Interval('1y') - Interval('1m')), '+ 11m')
+        ae(str(Interval('1:00') - Interval('- 1:00')), '+ 2:00')
+        ae(str(Interval('0:50') - Interval('- 0:50')), '+ 1:40')
+        ae(str(Interval('1:50') - Interval('1:50')), '')
+        ae(str(Interval('1:59:59') - Interval('- 00:00:01')), '+ 2:00')
+        ae(str(Interval('2:00') - Interval('00:00:01')), '+ 1:59:59')
+
+    def testOverflow(self):
+        ae = self.assertEqual
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, 60)), (1,0,0,0, 0, 1, 0))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, 100)), (1,0,0,0, 0, 1, 40))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, 60*60)), (1,0,0,0, 1, 0, 0))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, 24*60*60)), (1,0,0,1, 0, 0, 0))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, -1)), (-1,0,0,0, 0, 0, 1))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, -100)), (-1,0,0,0, 0, 1, 40))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, -60*60)), (-1,0,0,0, 1, 0, 0))
+        ae(fixTimeOverflow((1,0,0,0, 0, 0, -24*60*60)), (-1,0,0,1, 0, 0, 0))
+        ae(fixTimeOverflow((-1,0,0,0, 0, 0, 1)), (-1,0,0,0, 0, 0, 1))
+        ae(fixTimeOverflow((-1,0,0,0, 0, 0, 100)), (-1,0,0,0, 0, 1, 40))
+        ae(fixTimeOverflow((-1,0,0,0, 0, 0, 60*60)), (-1,0,0,0, 1, 0, 0))
+        ae(fixTimeOverflow((-1,0,0,0, 0, 0, 24*60*60)), (-1,0,0,1, 0, 0, 0))
+
+    def testDivision(self):
+        ae = self.assertEqual
+        ae(str(Interval('1y')/2), '+ 6m')
+        ae(str(Interval('1:00')/2), '+ 0:30')
+        ae(str(Interval('00:01')/2), '+ 0:00:30')
 
 def suite():
    return unittest.makeSuite(DateTestCase, 'test')