Skip to content

Commit a3d91ed

Browse files
author
Richard Jones
committed
fixed Interval maths [SF#665357]
1 parent 76a106b commit a3d91ed

File tree

3 files changed

+184
-27
lines changed

3 files changed

+184
-27
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Fixed:
3131
- re-worked detectors initialisation - woohoo, no more cross-importing!
3232
- fixed export/import of retired nodes (sf bug 685273)
3333
- fixed mutation of properties bug in RDBMS backends
34+
- fixed Interval maths (sf bug 665357)
3435

3536
Feature:
3637
- support setting of properties on message and file through web and

roundup/date.py

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: date.py,v 1.43 2003-02-23 19:05:14 kedder Exp $
18+
# $Id: date.py,v 1.44 2003-03-06 02:33:56 richard Exp $
1919

2020
__doc__ = """
2121
Date, time and time interval handling.
2222
"""
2323

24-
import time, re, calendar
24+
import time, re, calendar, types
2525
from i18n import _
2626

2727
class Date:
@@ -306,14 +306,28 @@ class Interval:
306306
307307
Example usage:
308308
>>> Interval(" 3w 1 d 2:00")
309-
<Interval 22d 2:00>
309+
<Interval + 22d 2:00>
310310
>>> Date(". + 2d") + Interval("- 3w")
311311
<Date 2000-06-07.00:34:02>
312-
313-
Intervals are added/subtracted in order of:
312+
>>> Interval('1:59:59') + Interval('00:00:01')
313+
<Interval + 2:00>
314+
>>> Interval('2:00') + Interval('- 00:00:01')
315+
<Interval + 1:59:59>
316+
>>> Interval('1y')/2
317+
<Interval + 6m>
318+
>>> Interval('1:00')/2
319+
<Interval + 0:30>
320+
321+
Interval arithmetic is handled in a couple of special ways, trying
322+
to cater for the most common cases. Fundamentally, Intervals which
323+
have both date and time parts will result in strange results in
324+
arithmetic - because of the impossibility of handling day->month->year
325+
over- and under-flows. Intervals may also be divided by some number.
326+
327+
Intervals are added to Dates in order of:
314328
seconds, minutes, hours, years, months, days
315329
316-
Calculations involving monts (eg '+2m') have no effect on days - only
330+
Calculations involving months (eg '+2m') have no effect on days - only
317331
days (or over/underflow from hours/mins/secs) will do that, and
318332
days-per-month and leap years are accounted for. Leap seconds are not.
319333
@@ -350,15 +364,16 @@ def __cmp__(self, other):
350364

351365
def __str__(self):
352366
"""Return this interval as a string."""
353-
sign = {1:'+', -1:'-'}[self.sign]
354-
l = [sign]
367+
l = []
355368
if self.year: l.append('%sy'%self.year)
356369
if self.month: l.append('%sm'%self.month)
357370
if self.day: l.append('%sd'%self.day)
358371
if self.second:
359372
l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
360373
elif self.hour or self.minute:
361374
l.append('%d:%02d'%(self.hour, self.minute))
375+
if l:
376+
l.insert(0, {1:'+', -1:'-'}[self.sign])
362377
return ' '.join(l)
363378

364379
def __add__(self, other):
@@ -368,15 +383,78 @@ def __add__(self, other):
368383
elif isinstance(other, Interval):
369384
# add the other Interval to this one
370385
a = self.get_tuple()
386+
as = a[0]
371387
b = other.get_tuple()
372-
if b[0] < 0:
373-
i = Interval([x-y for x,y in zip(a[1:],b[1:])])
374-
else:
375-
i = Interval([x+y for x,y in zip(a[1:],b[1:])])
376-
return i
388+
bs = b[0]
389+
i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
390+
i.insert(0, 1)
391+
i = fixTimeOverflow(i)
392+
return Interval(i)
393+
# nope, no idea what to do with this other...
394+
raise TypeError, "Can't add %r"%other
395+
396+
def __sub__(self, other):
397+
if isinstance(other, Date):
398+
# the other is a Date - produce a Date
399+
interval = Interval(self.get_tuple())
400+
interval.sign *= -1
401+
return Date(other.addInterval(interval))
402+
elif isinstance(other, Interval):
403+
# add the other Interval to this one
404+
a = self.get_tuple()
405+
as = a[0]
406+
b = other.get_tuple()
407+
bs = b[0]
408+
i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
409+
i.insert(0, 1)
410+
i = fixTimeOverflow(i)
411+
return Interval(i)
377412
# nope, no idea what to do with this other...
378413
raise TypeError, "Can't add %r"%other
379414

415+
def __div__(self, other):
416+
''' Divide this interval by an int value.
417+
418+
Can't divide years and months sensibly in the _same_
419+
calculation as days/time, so raise an error in that situation.
420+
'''
421+
try:
422+
other = float(other)
423+
except TypeError:
424+
raise ValueError, "Can only divide Intervals by numbers"
425+
426+
y, m, d, H, M, S = (self.year, self.month, self.day,
427+
self.hour, self.minute, self.second)
428+
if y or m:
429+
if d or H or M or S:
430+
raise ValueError, "Can't divide Interval with date and time"
431+
months = self.year*12 + self.month
432+
months *= self.sign
433+
434+
months = int(months/other)
435+
436+
sign = months<0 and -1 or 1
437+
m = months%12
438+
y = months / 12
439+
return Interval((sign, y, m, 0, 0, 0, 0))
440+
441+
else:
442+
# handle a day/time division
443+
seconds = S + M*60 + H*60*60 + d*60*60*24
444+
seconds *= self.sign
445+
446+
seconds = int(seconds/other)
447+
448+
sign = seconds<0 and -1 or 1
449+
seconds *= sign
450+
S = seconds%60
451+
seconds /= 60
452+
M = seconds%60
453+
seconds /= 60
454+
H = seconds%24
455+
d = seconds / 24
456+
return Interval((sign, 0, 0, d, H, M, S))
457+
380458
def set(self, spec, interval_re=re.compile('''
381459
\s*(?P<s>[-+])? # + or -
382460
\s*((?P<y>\d+\s*)y)? # year
@@ -477,6 +555,38 @@ def serialise(self):
477555
return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
478556
self.day, self.hour, self.minute, self.second)
479557

558+
def fixTimeOverflow(time):
559+
''' Handle the overflow in the time portion (H, M, S) of "time":
560+
(sign, y,m,d,H,M,S)
561+
562+
Overflow and underflow will at most affect the _days_ portion of
563+
the date. We do not overflow days to months as we don't know _how_
564+
to, generally.
565+
'''
566+
# XXX we could conceivably use this function for handling regular dates
567+
# XXX too - we just need to interrogate the month/year for the day
568+
# XXX overflow...
569+
570+
sign, y, m, d, H, M, S = time
571+
seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
572+
if seconds:
573+
sign = seconds<0 and -1 or 1
574+
seconds *= sign
575+
S = seconds%60
576+
seconds /= 60
577+
M = seconds%60
578+
seconds /= 60
579+
H = seconds%24
580+
d = seconds / 24
581+
else:
582+
months = y*12 + m
583+
sign = months<0 and -1 or 1
584+
months *= sign
585+
m = months%12
586+
y = months/12
587+
588+
return (sign, y, m, d, H, M, S)
589+
480590

481591
def test():
482592
intervals = (" 3w 1 d 2:00", " + 2d", "3w")

test/test_dates.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: test_dates.py,v 1.17 2003-02-24 15:38:51 kedder Exp $
18+
# $Id: test_dates.py,v 1.18 2003-03-06 02:33:57 richard Exp $
1919

2020
import unittest, time
2121

22-
from roundup.date import Date, Interval
22+
from roundup.date import Date, Interval, fixTimeOverflow
2323

2424
class DateTestCase(unittest.TestCase):
2525
def testDateInterval(self):
@@ -71,7 +71,9 @@ def testOffset(self):
7171
date = Date("8:47:11", -5)
7272
ae(str(date), '%s-%02d-%02d.13:47:11'%(y, m, d))
7373

74-
# now check calculations
74+
def testOffsetRandom(self):
75+
ae = self.assertEqual
76+
# XXX unsure of the usefulness of these, they're pretty random
7577
date = Date('2000-01-01') + Interval('- 2y 2m')
7678
ae(str(date), '1997-11-01.00:00:00')
7779
date = Date('2000-01-01 - 2y 2m')
@@ -86,7 +88,8 @@ def testOffset(self):
8688
date = Date('2001-01-01') + Interval('60d')
8789
ae(str(date), '2001-03-02.00:00:00')
8890

89-
# time additions
91+
def testOffsetAdd(self):
92+
ae = self.assertEqual
9093
date = Date('2000-02-28.23:59:59') + Interval('00:00:01')
9194
ae(str(date), '2000-02-29.00:00:00')
9295
date = Date('2001-02-28.23:59:59') + Interval('00:00:01')
@@ -107,7 +110,8 @@ def testOffset(self):
107110
date = Date('2001-02-28.22:58:59') + Interval('00:00:3661')
108111
ae(str(date), '2001-03-01.00:00:00')
109112

110-
# now subtractions
113+
def testOffsetSub(self):
114+
ae = self.assertEqual
111115
date = Date('2000-01-01') - Interval('- 2y 2m')
112116
ae(str(date), '2002-03-01.00:00:00')
113117
date = Date('2000-01-01') - Interval('2m')
@@ -138,12 +142,14 @@ def testOffset(self):
138142
date = Date('2001-03-01.00:00:00') - Interval('00:00:3661')
139143
ae(str(date), '2001-02-28.22:58:59')
140144

141-
# local()
145+
def testDateLocal(self):
146+
ae = self.assertEqual
142147
date = Date("02:42:20")
143148
date = date.local(10)
149+
y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
144150
ae(str(date), '%s-%02d-%02d.12:42:20'%(y, m, d))
145151

146-
def testInterval(self):
152+
def testIntervalInit(self):
147153
ae = self.assertEqual
148154
ae(str(Interval('3y')), '+ 3y')
149155
ae(str(Interval('2 y 1 m')), '+ 2y 1m')
@@ -153,16 +159,56 @@ def testInterval(self):
153159
ae(str(Interval(' 14:00 ')), '+ 14:00')
154160
ae(str(Interval(' 0:04:33 ')), '+ 0:04:33')
155161

156-
# __add__
157-
# XXX these are fairly arbitrary and need fixing once the __add__
158-
# code handles the odd cases more correctly
162+
def testIntervalAdd(self):
163+
ae = self.assertEqual
159164
ae(str(Interval('1y') + Interval('1y')), '+ 2y')
160165
ae(str(Interval('1y') + Interval('1m')), '+ 1y 1m')
161166
ae(str(Interval('1y') + Interval('2:40')), '+ 1y 2:40')
162-
ae(str(Interval('1y') + Interval('- 1y')), '+')
163-
ae(str(Interval('1y') + Interval('- 1m')), '+ 1y -1m')
164-
165-
# TODO test add, subtraction, ?division?
167+
ae(str(Interval('1y') + Interval('- 1y')), '')
168+
ae(str(Interval('- 1y') + Interval('1y')), '')
169+
ae(str(Interval('- 1y') + Interval('- 1y')), '- 2y')
170+
ae(str(Interval('1y') + Interval('- 1m')), '+ 11m')
171+
ae(str(Interval('1:00') + Interval('1:00')), '+ 2:00')
172+
ae(str(Interval('0:50') + Interval('0:50')), '+ 1:40')
173+
ae(str(Interval('1:50') + Interval('- 1:50')), '')
174+
ae(str(Interval('- 1:50') + Interval('1:50')), '')
175+
ae(str(Interval('- 1:50') + Interval('- 1:50')), '- 3:40')
176+
ae(str(Interval('1:59:59') + Interval('00:00:01')), '+ 2:00')
177+
ae(str(Interval('2:00') + Interval('- 00:00:01')), '+ 1:59:59')
178+
179+
def testIntervalSub(self):
180+
ae = self.assertEqual
181+
ae(str(Interval('1y') - Interval('- 1y')), '+ 2y')
182+
ae(str(Interval('1y') - Interval('- 1m')), '+ 1y 1m')
183+
ae(str(Interval('1y') - Interval('- 2:40')), '+ 1y 2:40')
184+
ae(str(Interval('1y') - Interval('1y')), '')
185+
ae(str(Interval('1y') - Interval('1m')), '+ 11m')
186+
ae(str(Interval('1:00') - Interval('- 1:00')), '+ 2:00')
187+
ae(str(Interval('0:50') - Interval('- 0:50')), '+ 1:40')
188+
ae(str(Interval('1:50') - Interval('1:50')), '')
189+
ae(str(Interval('1:59:59') - Interval('- 00:00:01')), '+ 2:00')
190+
ae(str(Interval('2:00') - Interval('00:00:01')), '+ 1:59:59')
191+
192+
def testOverflow(self):
193+
ae = self.assertEqual
194+
ae(fixTimeOverflow((1,0,0,0, 0, 0, 60)), (1,0,0,0, 0, 1, 0))
195+
ae(fixTimeOverflow((1,0,0,0, 0, 0, 100)), (1,0,0,0, 0, 1, 40))
196+
ae(fixTimeOverflow((1,0,0,0, 0, 0, 60*60)), (1,0,0,0, 1, 0, 0))
197+
ae(fixTimeOverflow((1,0,0,0, 0, 0, 24*60*60)), (1,0,0,1, 0, 0, 0))
198+
ae(fixTimeOverflow((1,0,0,0, 0, 0, -1)), (-1,0,0,0, 0, 0, 1))
199+
ae(fixTimeOverflow((1,0,0,0, 0, 0, -100)), (-1,0,0,0, 0, 1, 40))
200+
ae(fixTimeOverflow((1,0,0,0, 0, 0, -60*60)), (-1,0,0,0, 1, 0, 0))
201+
ae(fixTimeOverflow((1,0,0,0, 0, 0, -24*60*60)), (-1,0,0,1, 0, 0, 0))
202+
ae(fixTimeOverflow((-1,0,0,0, 0, 0, 1)), (-1,0,0,0, 0, 0, 1))
203+
ae(fixTimeOverflow((-1,0,0,0, 0, 0, 100)), (-1,0,0,0, 0, 1, 40))
204+
ae(fixTimeOverflow((-1,0,0,0, 0, 0, 60*60)), (-1,0,0,0, 1, 0, 0))
205+
ae(fixTimeOverflow((-1,0,0,0, 0, 0, 24*60*60)), (-1,0,0,1, 0, 0, 0))
206+
207+
def testDivision(self):
208+
ae = self.assertEqual
209+
ae(str(Interval('1y')/2), '+ 6m')
210+
ae(str(Interval('1:00')/2), '+ 0:30')
211+
ae(str(Interval('00:01')/2), '+ 0:00:30')
166212

167213
def suite():
168214
return unittest.makeSuite(DateTestCase, 'test')

0 commit comments

Comments
 (0)