Skip to content

Commit 5d31a3c

Browse files
author
Richard Jones
committed
backport Interval fix from HEAD
1 parent 9096afb commit 5d31a3c

File tree

3 files changed

+195
-29
lines changed

3 files changed

+195
-29
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
This file contains the changes to the Roundup system over time. The entries
22
are given with the most recent entry first.
33

4+
2003-??-?? 0.5.7
5+
- fixed Interval maths (sf bug 665357)
6+
47
2003-02-27 0.5.6
58
- fixed templating filter function arguments (sf bug 678911)
69
- fixed multiselect in searching (sf bug 676874)

roundup/date.py

Lines changed: 130 additions & 15 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.40 2002-12-18 00:15:53 richard Exp $
18+
# $Id: date.py,v 1.40.2.1 2003-03-06 04:37:51 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:
@@ -253,8 +253,9 @@ def set(self, spec, offset=0, date_re=re.compile(r'''
253253
d = int(info['d'])
254254
if info['y'] is not None:
255255
y = int(info['y'])
256-
# time defaults to 00:00:00 now
257-
H = M = S = 0
256+
# time defaults to 00:00:00 GMT - offset (local midnight)
257+
H = -offset
258+
M = S = 0
258259

259260
# override hour, minute, second parts
260261
if info['H'] is not None and info['M'] is not None:
@@ -305,14 +306,28 @@ class Interval:
305306
306307
Example usage:
307308
>>> Interval(" 3w 1 d 2:00")
308-
<Interval 22d 2:00>
309+
<Interval + 22d 2:00>
309310
>>> Date(". + 2d") + Interval("- 3w")
310311
<Date 2000-06-07.00:34:02>
311-
312-
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:
313328
seconds, minutes, hours, years, months, days
314329
315-
Calculations involving monts (eg '+2m') have no effect on days - only
330+
Calculations involving months (eg '+2m') have no effect on days - only
316331
days (or over/underflow from hours/mins/secs) will do that, and
317332
days-per-month and leap years are accounted for. Leap seconds are not.
318333
@@ -349,15 +364,16 @@ def __cmp__(self, other):
349364

350365
def __str__(self):
351366
"""Return this interval as a string."""
352-
sign = {1:'+', -1:'-'}[self.sign]
353-
l = [sign]
367+
l = []
354368
if self.year: l.append('%sy'%self.year)
355369
if self.month: l.append('%sm'%self.month)
356370
if self.day: l.append('%sd'%self.day)
357371
if self.second:
358372
l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
359373
elif self.hour or self.minute:
360374
l.append('%d:%02d'%(self.hour, self.minute))
375+
if l:
376+
l.insert(0, {1:'+', -1:'-'}[self.sign])
361377
return ' '.join(l)
362378

363379
def __add__(self, other):
@@ -367,15 +383,78 @@ def __add__(self, other):
367383
elif isinstance(other, Interval):
368384
# add the other Interval to this one
369385
a = self.get_tuple()
386+
as = a[0]
370387
b = other.get_tuple()
371-
if b[0] < 0:
372-
i = Interval([x-y for x,y in zip(a[1:],b[1:])])
373-
else:
374-
i = Interval([x+y for x,y in zip(a[1:],b[1:])])
375-
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)
376393
# nope, no idea what to do with this other...
377394
raise TypeError, "Can't add %r"%other
378395

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)
412+
# nope, no idea what to do with this other...
413+
raise TypeError, "Can't add %r"%other
414+
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+
379458
def set(self, spec, interval_re=re.compile('''
380459
\s*(?P<s>[-+])? # + or -
381460
\s*((?P<y>\d+\s*)y)? # year
@@ -461,6 +540,10 @@ def pretty(self):
461540
s = _('1/2 an hour')
462541
else:
463542
s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
543+
if self.sign < 0:
544+
s = s + _(' ago')
545+
else:
546+
s = _('in') + s
464547
return s
465548

466549
def get_tuple(self):
@@ -472,6 +555,38 @@ def serialise(self):
472555
return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
473556
self.day, self.hour, self.minute, self.second)
474557

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+
475590

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

test/test_dates.py

Lines changed: 62 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.15 2002-12-09 02:43:21 richard Exp $
18+
# $Id: test_dates.py,v 1.15.2.1 2003-03-06 04:37:51 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):
@@ -56,10 +56,10 @@ def testDate(self):
5656
def testOffset(self):
5757
ae = self.assertEqual
5858
date = Date("2000-04-17", -5)
59-
ae(str(date), '2000-04-17.00:00:00')
59+
ae(str(date), '2000-04-17.05:00:00')
6060
date = Date("01-25", -5)
6161
y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
62-
ae(str(date), '%s-01-25.00:00:00'%y)
62+
ae(str(date), '%s-01-25.05:00:00'%y)
6363
date = Date("2000-04-17.03:45", -5)
6464
ae(str(date), '2000-04-17.08:45:00')
6565
date = Date("08-13.22:13", -5)
@@ -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,14 +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')
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')
164212

165213
def suite():
166214
return unittest.makeSuite(DateTestCase, 'test')

0 commit comments

Comments
 (0)