From d5307f519ea0861c98f59364b7343bf88ec45ddb Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 6 Mar 2003 02:33:57 +0000 Subject: [PATCH] fixed Interval maths (sf bug 665357) git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1564 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 1 + roundup/date.py | 136 ++++++++++++++++++++++++++++++++++++++++----- test/test_dates.py | 74 +++++++++++++++++++----- 3 files changed, 184 insertions(+), 27 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7aaf1d3..06a234b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/roundup/date.py b/roundup/date.py index a5bb8a9..04bcbda 100644 --- a/roundup/date.py +++ b/roundup/date.py @@ -15,13 +15,13 @@ # 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") - + >>> Date(". + 2d") + Interval("- 3w") - - Intervals are added/subtracted in order of: + >>> Interval('1:59:59') + Interval('00:00:01') + + >>> Interval('2:00') + Interval('- 00:00:01') + + >>> Interval('1y')/2 + + >>> Interval('1:00')/2 + + + 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[-+])? # + or - \s*((?P\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") diff --git a/test/test_dates.py b/test/test_dates.py index b16a8ca..f88212a 100644 --- a/test/test_dates.py +++ b/test/test_dates.py @@ -15,11 +15,11 @@ # 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') -- 2.30.2