Skip to content

Commit e328789

Browse files
author
Richard Jones
committed
fixed some problems in date calculations
(calendar.py doesn't handle over- and under-flow). Also, hour/minute/second intervals may now be more than 99 each.
1 parent 81601ad commit e328789

File tree

5 files changed

+167
-62
lines changed

5 files changed

+167
-62
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Fixed:
3737
. #517906 ] Attribute order in "View customisation"
3838
. #514854 ] History: "User" is always ticket creator
3939
. wasn't handling cvs parser feeding correctly
40+
. fixed some problems in date calculations (calendar.py doesn't handle over-
41+
and under-flow). Also, hour/minute/second intervals may now be more than
42+
99 each.
4043

4144

4245
2002-01-24 - 0.4.0

roundup/date.py

Lines changed: 101 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: date.py,v 1.18 2002-01-23 20:00:50 jhermann Exp $
18+
# $Id: date.py,v 1.19 2002-02-21 23:11:45 richard Exp $
1919

2020
__doc__ = """
2121
Date, time and time interval handling.
@@ -104,14 +104,52 @@ def applyInterval(self, interval):
104104
self.second, x, x, x = time.gmtime(calendar.timegm(t))
105105

106106
def __add__(self, other):
107-
"""Add an interval to this date to produce another date."""
108-
t = (self.year + other.sign * other.year,
109-
self.month + other.sign * other.month,
110-
self.day + other.sign * other.day,
111-
self.hour + other.sign * other.hour,
112-
self.minute + other.sign * other.minute,
113-
self.second + other.sign * other.second, 0, 0, 0)
114-
return Date(time.gmtime(calendar.timegm(t)))
107+
"""Add an interval to this date to produce another date.
108+
"""
109+
# do the basic calc
110+
sign = other.sign
111+
year = self.year + sign * other.year
112+
month = self.month + sign * other.month
113+
day = self.day + sign * other.day
114+
hour = self.hour + sign * other.hour
115+
minute = self.minute + sign * other.minute
116+
second = self.second + sign * other.second
117+
118+
# now cope with under- and over-flow
119+
# first do the time
120+
while (second < 0 or second > 59 or minute < 0 or minute > 59 or
121+
hour < 0 or hour > 59):
122+
if second < 0: minute -= 1; second += 60
123+
elif second > 59: minute += 1; second -= 60
124+
if minute < 0: hour -= 1; minute += 60
125+
elif minute > 59: hour += 1; minute -= 60
126+
if hour < 0: day -= 1; hour += 60
127+
elif hour > 59: day += 1; hour -= 60
128+
129+
# fix up the month so we're within range
130+
while month < 1 or month > 12:
131+
if month < 1: year -= 1; month += 12
132+
if month > 12: year += 1; month -= 12
133+
134+
# now do the days, now that we know what month we're in
135+
mdays = calendar.mdays
136+
if month == 2 and calendar.isleap(year): month_days = 29
137+
else: month_days = mdays[month]
138+
while month < 1 or month > 12 or day < 0 or day > month_days:
139+
# now to day under/over
140+
if day < 0: month -= 1; day += month_days
141+
elif day > month_days: month += 1; day -= month_days
142+
143+
# possibly fix up the month so we're within range
144+
while month < 1 or month > 12:
145+
if month < 1: year -= 1; month += 12
146+
if month > 12: year += 1; month -= 12
147+
148+
# re-figure the number of days for this month
149+
if month == 2 and calendar.isleap(year): month_days = 29
150+
else: month_days = mdays[month]
151+
152+
return Date((year, month, day, hour, minute, second, 0, 0, 0))
115153

116154
# XXX deviates from spec to allow subtraction of dates as well
117155
def __sub__(self, other):
@@ -124,14 +162,14 @@ def __sub__(self, other):
124162
# leap years, phases of the moon, ....
125163
a = calendar.timegm((self.year, self.month, self.day, self.hour,
126164
self.minute, self.second, 0, 0, 0))
127-
b = calendar.timegm((other.year, other.month, other.day, other.hour,
128-
other.minute, other.second, 0, 0, 0))
165+
b = calendar.timegm((other.year, other.month, other.day,
166+
other.hour, other.minute, other.second, 0, 0, 0))
129167
diff = a - b
130168
if diff < 0:
131-
sign = -1
169+
sign = 1
132170
diff = -diff
133171
else:
134-
sign = 1
172+
sign = -1
135173
S = diff%60
136174
M = (diff/60)%60
137175
H = (diff/(60*60))%60
@@ -143,13 +181,7 @@ def __sub__(self, other):
143181
y = (diff/(365*24*60*60))
144182
if y>1: d = H = S = M = 0
145183
return Interval((y, m, d, H, M, S), sign=sign)
146-
t = (self.year - other.sign * other.year,
147-
self.month - other.sign * other.month,
148-
self.day - other.sign * other.day,
149-
self.hour - other.sign * other.hour,
150-
self.minute - other.sign * other.minute,
151-
self.second - other.sign * other.second, 0, 0, 0)
152-
return Date(time.gmtime(calendar.timegm(t)))
184+
return self.__add__(other)
153185

154186
def __cmp__(self, other):
155187
"""Compare this date to another date."""
@@ -244,8 +276,17 @@ class Interval:
244276
Example usage:
245277
>>> Interval(" 3w 1 d 2:00")
246278
<Interval 22d 2:00>
247-
>>> Date(". + 2d") - Interval("3w")
279+
>>> Date(". + 2d") + Interval("- 3w")
248280
<Date 2000-06-07.00:34:02>
281+
282+
Intervals are added/subtracted in order of:
283+
seconds, minutes, hours, years, months, days
284+
285+
Calculations involving monts (eg '+2m') have no effect on days - only
286+
days (or over/underflow from hours/mins/secs) will do that, and
287+
days-per-month and leap years are accounted for. Leap seconds are not.
288+
289+
TODO: more examples, showing the order of addition operation
249290
'''
250291
def __init__(self, spec, sign=1):
251292
"""Construct an interval given a specification."""
@@ -290,7 +331,7 @@ def set(self, spec, interval_re = re.compile('''
290331
\s*
291332
((?P<d>\d+\s*)d)? # day
292333
\s*
293-
(((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # time
334+
(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
294335
\s*
295336
''', re.VERBOSE)):
296337
''' set the date to the value in spec
@@ -323,40 +364,47 @@ def pretty(self):
323364
'''
324365
if self.year or self.month > 2:
325366
return None
326-
if self.month or self.day > 13:
367+
elif self.month or self.day > 13:
327368
days = (self.month * 30) + self.day
328369
if days > 28:
329370
if int(days/30) > 1:
330-
return _('%(number)s months')%{'number': int(days/30)}
371+
s = _('%(number)s months')%{'number': int(days/30)}
331372
else:
332-
return _('1 month')
373+
s = _('1 month')
374+
else:
375+
s = _('%(number)s weeks')%{'number': int(days/7)}
376+
elif self.day > 7:
377+
s = _('1 week')
378+
elif self.day > 1:
379+
s = _('%(number)s days')%{'number': self.day}
380+
elif self.day == 1 or self.hour > 12:
381+
if self.sign > 0:
382+
return _('tomorrow')
333383
else:
334-
return _('%(number)s weeks')%{'number': int(days/7)}
335-
if self.day > 7:
336-
return _('1 week')
337-
if self.day > 1:
338-
return _('%(number)s days')%{'number': self.day}
339-
if self.day == 1 or self.hour > 12:
340-
return _('yesterday')
341-
if self.hour > 1:
342-
return _('%(number)s hours')%{'number': self.hour}
343-
if self.hour == 1:
384+
return _('yesterday')
385+
elif self.hour > 1:
386+
s = _('%(number)s hours')%{'number': self.hour}
387+
elif self.hour == 1:
344388
if self.minute < 15:
345-
return _('an hour')
346-
quart = self.minute/15
347-
if quart == 2:
348-
return _('1 1/2 hours')
349-
return _('1 %(number)s/4 hours')%{'number': quart}
350-
if self.minute < 1:
351-
return _('just now')
352-
if self.minute == 1:
353-
return _('1 minute')
354-
if self.minute < 15:
355-
return _('%(number)s minutes')%{'number': self.minute}
356-
quart = int(self.minute/15)
357-
if quart == 2:
358-
return _('1/2 an hour')
359-
return _('%(number)s/4 hour')%{'number': quart}
389+
s = _('an hour')
390+
elif self.minute/15 == 2:
391+
s = _('1 1/2 hours')
392+
else:
393+
s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
394+
elif self.minute < 1:
395+
if self.sign > 0:
396+
return _('in a moment')
397+
else:
398+
return _('just now')
399+
elif self.minute == 1:
400+
s = _('1 minute')
401+
elif self.minute < 15:
402+
s = _('%(number)s minutes')%{'number': self.minute}
403+
elif int(self.minute/15) == 2:
404+
s = _('1/2 an hour')
405+
else:
406+
s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
407+
return s
360408

361409
def get_tuple(self):
362410
return (self.year, self.month, self.day, self.hour, self.minute,
@@ -385,6 +433,9 @@ def test():
385433

386434
#
387435
# $Log: not supported by cvs2svn $
436+
# Revision 1.18 2002/01/23 20:00:50 jhermann
437+
# %e is a UNIXism and not documented for Python
438+
#
388439
# Revision 1.17 2002/01/16 07:02:57 richard
389440
# . lots of date/interval related changes:
390441
# - more relaxed date format for input

roundup/htmltemplate.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: htmltemplate.py,v 1.81 2002-02-21 07:21:38 richard Exp $
18+
# $Id: htmltemplate.py,v 1.82 2002-02-21 23:11:45 richard Exp $
1919

2020
__doc__ = """
2121
Template engine.
@@ -400,7 +400,7 @@ def do_reldate(self, property, pretty=0):
400400
return ''
401401

402402
# figure the interval
403-
interval = value - date.Date('.')
403+
interval = date.Date('.') - value
404404
if pretty:
405405
if not self.nodeid:
406406
return _('now')
@@ -1091,6 +1091,9 @@ def render(self, form):
10911091

10921092
#
10931093
# $Log: not supported by cvs2svn $
1094+
# Revision 1.81 2002/02/21 07:21:38 richard
1095+
# docco
1096+
#
10941097
# Revision 1.80 2002/02/21 07:19:08 richard
10951098
# ... and label, width and height control for extra flavour!
10961099
#

test/test_dates.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
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.9 2002-02-21 06:57:39 richard Exp $
18+
# $Id: test_dates.py,v 1.10 2002-02-21 23:11:45 richard Exp $
1919

2020
import unittest, time
2121

@@ -70,8 +70,37 @@ def testOffset(self):
7070
ae(str(date), '%s-%02d-%02d.19:25:00'%(y, m, d))
7171
date = Date("8:47:11", -5)
7272
ae(str(date), '%s-%02d-%02d.13:47:11'%(y, m, d))
73-
# TODO: assert something
74-
Date() + Interval('- 2y 2m')
73+
74+
# now check calculations
75+
date = Date('2000-01-01') + Interval('- 2y 2m')
76+
ae(str(date), '1997-11-01.00:00:00')
77+
date = Date('2000-01-01') + Interval('+ 2m')
78+
ae(str(date), '2000-03-01.00:00:00')
79+
80+
date = Date('2000-01-01') + Interval('60d')
81+
ae(str(date), '2000-03-01.00:00:00')
82+
date = Date('2001-01-01') + Interval('60d')
83+
ae(str(date), '2001-03-02.00:00:00')
84+
85+
date = Date('2000-02-28.23:59:59') + Interval('00:00:01')
86+
ae(str(date), '2000-02-29.00:00:00')
87+
date = Date('2001-02-28.23:59:59') + Interval('00:00:01')
88+
ae(str(date), '2001-03-01.00:00:00')
89+
90+
date = Date('2000-02-28.23:58:59') + Interval('00:01:01')
91+
ae(str(date), '2000-02-29.00:00:00')
92+
date = Date('2001-02-28.23:58:59') + Interval('00:01:01')
93+
ae(str(date), '2001-03-01.00:00:00')
94+
95+
date = Date('2000-02-28.22:58:59') + Interval('01:01:01')
96+
ae(str(date), '2000-02-29.00:00:00')
97+
date = Date('2001-02-28.22:58:59') + Interval('01:01:01')
98+
ae(str(date), '2001-03-01.00:00:00')
99+
100+
date = Date('2000-02-28.22:58:59') + Interval('00:00:3661')
101+
ae(str(date), '2000-02-29.00:00:00')
102+
date = Date('2001-02-28.22:58:59') + Interval('00:00:3661')
103+
ae(str(date), '2001-03-01.00:00:00')
75104

76105
def testInterval(self):
77106
ae = self.assertEqual
@@ -89,6 +118,14 @@ def suite():
89118

90119
#
91120
# $Log: not supported by cvs2svn $
121+
# Revision 1.9 2002/02/21 06:57:39 richard
122+
# . Added popup help for classes using the classhelp html template function.
123+
# - add <display call="classhelp('priority', 'id,name,description')">
124+
# to an item page, and it generates a link to a popup window which displays
125+
# the id, name and description for the priority class. The description
126+
# field won't exist in most installations, but it will be added to the
127+
# default templates.
128+
#
92129
# Revision 1.8 2002/01/16 07:02:57 richard
93130
# . lots of date/interval related changes:
94131
# - more relaxed date format for input

test/test_htmltemplate.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# but WITHOUT ANY WARRANTY; without even the implied warranty of
99
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1010
#
11-
# $Id: test_htmltemplate.py,v 1.10 2002-02-21 06:57:39 richard Exp $
11+
# $Id: test_htmltemplate.py,v 1.11 2002-02-21 23:11:45 richard Exp $
1212

1313
import unittest, cgi, time
1414

@@ -24,7 +24,9 @@ def get(self, nodeid, attribute, default=None):
2424
elif attribute == 'filename':
2525
return 'file.foo'
2626
elif attribute == 'date':
27-
return date.Date() + date.Interval('- 2y 2m')
27+
return date.Date('2000-01-01')
28+
elif attribute == 'reldate':
29+
return date.Date() + date.Interval('- 2y 1m')
2830
elif attribute == 'interval':
2931
return date.Interval('-3d')
3032
elif attribute == 'link':
@@ -45,7 +47,8 @@ def getprops(self):
4547
return {'string': String(), 'date': Date(), 'interval': Interval(),
4648
'link': Link('other'), 'multilink': Multilink('other'),
4749
'password': Password(), 'html': String(), 'key': String(),
48-
'novalue': String(), 'filename': String(), 'multiline': String()}
50+
'novalue': String(), 'filename': String(), 'multiline': String(),
51+
'reldate': Date()}
4952
def labelprop(self):
5053
return 'key'
5154

@@ -257,9 +260,9 @@ def testReldate_nondate(self):
257260
self.assertEqual(self.tf.do_reldate('multilink'), s)
258261

259262
def testReldate_date(self):
260-
self.assertEqual(self.tf.do_reldate('date'), '- 2y 1m')
261-
date = self.tf.cl.get('1', 'date')
262-
self.assertEqual(self.tf.do_reldate('date', pretty=1), date.pretty())
263+
self.assertEqual(self.tf.do_reldate('reldate'), '- 2y 1m')
264+
date = self.tf.cl.get('1', 'reldate')
265+
self.assertEqual(self.tf.do_reldate('reldate', pretty=1), date.pretty())
263266

264267
# def do_download(self, property):
265268
def testDownload_novalue(self):
@@ -333,14 +336,22 @@ def testList_multilink(self):
333336
def testClasshelp(self):
334337
self.assertEqual(self.tf.do_classhelp('theclass', 'prop1,prop2'),
335338
'<a href="javascript:help_window(\'classhelp?classname=theclass'
336-
'&properties=prop1,prop2\')"><b>(?)</b></a>')
339+
'&properties=prop1,prop2\', \'400\', \'400\')"><b>(?)</b></a>')
337340

338341
def suite():
339342
return unittest.makeSuite(NodeCase, 'test')
340343

341344

342345
#
343346
# $Log: not supported by cvs2svn $
347+
# Revision 1.10 2002/02/21 06:57:39 richard
348+
# . Added popup help for classes using the classhelp html template function.
349+
# - add <display call="classhelp('priority', 'id,name,description')">
350+
# to an item page, and it generates a link to a popup window which displays
351+
# the id, name and description for the priority class. The description
352+
# field won't exist in most installations, but it will be added to the
353+
# default templates.
354+
#
344355
# Revision 1.9 2002/02/15 07:08:45 richard
345356
# . Alternate email addresses are now available for users. See the MIGRATION
346357
# file for info on how to activate the feature.

0 commit comments

Comments
 (0)