Code

Fixed the 2.1-specific gmtime() (no arg) call in roundup.date. (Paul Wright)
[roundup.git] / roundup / date.py
1 # $Id: date.py,v 1.6 2001-07-31 09:54:18 richard Exp $
3 import time, re, calendar
5 class Date:
6     '''
7     As strings, date-and-time stamps are specified with the date in
8     international standard format (yyyy-mm-dd) joined to the time
9     (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
10     and are fairly readable when printed. An example of a valid stamp is
11     "2000-06-24.13:03:59". We'll call this the "full date format". When
12     Timestamp objects are printed as strings, they appear in the full date
13     format with the time always given in GMT. The full date format is
14     always exactly 19 characters long. 
16     For user input, some partial forms are also permitted: the whole time
17     or just the seconds may be omitted; and the whole date may be omitted
18     or just the year may be omitted. If the time is given, the time is
19     interpreted in the user's local time zone. The Date constructor takes
20     care of these conversions. In the following examples, suppose that yyyy
21     is the current year, mm is the current month, and dd is the current day
22     of the month; and suppose that the user is on Eastern Standard Time.
24       "2000-04-17" means <Date 2000-04-17.00:00:00>
25       "01-25" means <Date yyyy-01-25.00:00:00>
26       "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
27       "08-13.22:13" means <Date yyyy-08-14.03:13:00>
28       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
29       "14:25" means <Date yyyy-mm-dd.19:25:00>
30       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
31       "." means "right now"
33     The Date class should understand simple date expressions of the form
34     stamp + interval and stamp - interval. When adding or subtracting
35     intervals involving months or years, the components are handled
36     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
37     first add one month to get 2000-07-25, then add 10 days to get
38     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
39     or 41 days).
41     Example usage:
42         >>> Date(".")
43         <Date 2000-06-26.00:34:02>
44         >>> _.local(-5)
45         "2000-06-25.19:34:02"
46         >>> Date(". + 2d")
47         <Date 2000-06-28.00:34:02>
48         >>> Date("1997-04-17", -5)
49         <Date 1997-04-17.00:00:00>
50         >>> Date("01-25", -5)
51         <Date 2000-01-25.00:00:00>
52         >>> Date("08-13.22:13", -5)
53         <Date 2000-08-14.03:13:00>
54         >>> Date("14:25", -5)
55         <Date 2000-06-25.19:25:00>
56     '''
57     isDate = 1
59     def __init__(self, spec='.', offset=0):
60         """Construct a date given a specification and a time zone offset.
62           'spec' is a full date or a partial form, with an optional
63                  added or subtracted interval. Or a date 9-tuple.
64         'offset' is the local time zone offset from GMT in hours.
65         """
66         if type(spec) == type(''):
67             self.set(spec, offset=offset)
68         else:
69             y,m,d,H,M,S,x,x,x = spec
70             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
71             self.year, self.month, self.day, self.hour, self.minute, \
72                 self.second, x, x, x = time.gmtime(ts)
74     def applyInterval(self, interval):
75         ''' Apply the interval to this date
76         '''
77         t = (self.year + interval.year,
78              self.month + interval.month,
79              self.day + interval.day,
80              self.hour + interval.hour,
81              self.minute + interval.minute,
82              self.second + interval.second, 0, 0, 0)
83         self.year, self.month, self.day, self.hour, self.minute, \
84             self.second, x, x, x = time.gmtime(calendar.timegm(t))
86     def __add__(self, other):
87         """Add an interval to this date to produce another date."""
88         t = (self.year + other.sign * other.year,
89             self.month + other.sign * other.month,
90             self.day + other.sign * other.day,
91             self.hour + other.sign * other.hour,
92             self.minute + other.sign * other.minute,
93             self.second + other.sign * other.second, 0, 0, 0)
94         return Date(time.gmtime(calendar.timegm(t)))
96     # XXX deviates from spec to allow subtraction of dates as well
97     def __sub__(self, other):
98         """ Subtract:
99              1. an interval from this date to produce another date.
100              2. a date from this date to produce an interval.
101         """
102         if other.isDate:
103             # TODO this code will fall over laughing if the dates cross
104             # leap years, phases of the moon, ....
105             a = calendar.timegm((self.year, self.month, self.day, self.hour,
106                 self.minute, self.second, 0, 0, 0))
107             b = calendar.timegm((other.year, other.month, other.day, other.hour,
108                 other.minute, other.second, 0, 0, 0))
109             diff = a - b
110             if diff < 0:
111                 sign = -1
112                 diff = -diff
113             else:
114                 sign = 1
115             S = diff%60
116             M = (diff/60)%60
117             H = (diff/(60*60))%60
118             if H>1: S = 0
119             d = (diff/(24*60*60))%30
120             if d>1: H = S = M = 0
121             m = (diff/(30*24*60*60))%12
122             if m>1: H = S = M = 0
123             y = (diff/(365*24*60*60))
124             if y>1: d = H = S = M = 0
125             return Interval((y, m, d, H, M, S), sign=sign)
126         t = (self.year - other.sign * other.year,
127              self.month - other.sign * other.month,
128              self.day - other.sign * other.day,
129              self.hour - other.sign * other.hour,
130              self.minute - other.sign * other.minute,
131              self.second - other.sign * other.second, 0, 0, 0)
132         return Date(time.gmtime(calendar.timegm(t)))
134     def __cmp__(self, other):
135         """Compare this date to another date."""
136         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
137             r = cmp(getattr(self, attr), getattr(other, attr))
138             if r: return r
139         return 0
141     def __str__(self):
142         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
143         return time.strftime('%Y-%m-%d.%T', (self.year, self.month, self.day,
144             self.hour, self.minute, self.second, 0, 0, 0))
146     def pretty(self):
147         ''' print up the date date using a pretty format...
148         '''
149         return time.strftime('%e %B %Y', (self.year, self.month,
150             self.day, self.hour, self.minute, self.second, 0, 0, 0))
152     def set(self, spec, offset=0, date_re=re.compile(r'''
153               (((?P<y>\d\d\d\d)-)?((?P<m>\d\d)-(?P<d>\d\d))?)? # yyyy-mm-dd
154               (?P<n>\.)?                                       # .
155               (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?    # hh:mm:ss
156               (?P<o>.+)?                                       # offset
157               ''', re.VERBOSE)):
158         ''' set the date to the value in spec
159         '''
160         m = date_re.match(spec)
161         if not m:
162             raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]'
163         info = m.groupdict()
165         # get the current date/time using the offset
166         y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
168         # override year, month, day parts
169         if info['m'] is not None and info['d'] is not None:
170             m = int(info['m'])
171             d = int(info['d'])
172             if info['y'] is not None: y = int(info['y'])
173             H = M = S = 0
175         # override hour, minute, second parts
176         if info['H'] is not None and info['M'] is not None:
177             H = int(info['H']) - offset
178             M = int(info['M'])
179             S = 0
180             if info['S'] is not None: S = int(info['S'])
182         # now handle the adjustment of hour
183         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
184         self.year, self.month, self.day, self.hour, self.minute, \
185             self.second, x, x, x = time.gmtime(ts)
187         if info['o']:
188             self.applyInterval(Interval(info['o']))
190     def __repr__(self):
191         return '<Date %s>'%self.__str__()
193     def local(self, offset):
194         """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
195         t = (self.year, self.month, self.day, self.hour + offset, self.minute,
196              self.second, 0, 0, 0)
197         self.year, self.month, self.day, self.hour, self.minute, \
198             self.second, x, x, x = time.gmtime(calendar.timegm(t))
200     def get_tuple(self):
201         return (self.year, self.month, self.day, self.hour, self.minute,
202             self.second, 0, 0, 0)
204 class Interval:
205     '''
206     Date intervals are specified using the suffixes "y", "m", and "d". The
207     suffix "w" (for "week") means 7 days. Time intervals are specified in
208     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
209     may not).
211       "3y" means three years
212       "2y 1m" means two years and one month
213       "1m 25d" means one month and 25 days
214       "2w 3d" means two weeks and three days
215       "1d 2:50" means one day, two hours, and 50 minutes
216       "14:00" means 14 hours
217       "0:04:33" means four minutes and 33 seconds
219     Example usage:
220         >>> Interval("  3w  1  d  2:00")
221         <Interval 22d 2:00>
222         >>> Date(". + 2d") - Interval("3w")
223         <Date 2000-06-07.00:34:02>
224     '''
225     isInterval = 1
227     def __init__(self, spec, sign=1):
228         """Construct an interval given a specification."""
229         if type(spec) == type(''):
230             self.set(spec)
231         else:
232             self.sign = sign
233             self.year, self.month, self.day, self.hour, self.minute, \
234                 self.second = spec
236     def __cmp__(self, other):
237         """Compare this interval to another interval."""
238         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
239             r = cmp(getattr(self, attr), getattr(other, attr))
240             if r: return r
241         return 0
242         
243     def __str__(self):
244         """Return this interval as a string."""
245         sign = {1:'+', -1:'-'}[self.sign]
246         l = [sign]
247         if self.year: l.append('%sy'%self.year)
248         if self.month: l.append('%sm'%self.month)
249         if self.day: l.append('%sd'%self.day)
250         if self.second:
251             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
252         elif self.hour or self.minute:
253             l.append('%d:%02d'%(self.hour, self.minute))
254         return ' '.join(l)
256     def set(self, spec, interval_re = re.compile('''
257             \s*
258             (?P<s>[-+])?         # + or -
259             \s*
260             ((?P<y>\d+\s*)y)?    # year
261             \s*
262             ((?P<m>\d+\s*)m)?    # month
263             \s*
264             ((?P<w>\d+\s*)w)?    # week
265             \s*
266             ((?P<d>\d+\s*)d)?    # day
267             \s*
268             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?   # time
269             \s*
270             ''', re.VERBOSE)):
271         ''' set the date to the value in spec
272         '''
273         self.year = self.month = self.week = self.day = self.hour = \
274             self.minute = self.second = 0
275         self.sign = 1
276         m = interval_re.match(spec)
277         if not m:
278             raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]'
280         info = m.groupdict()
281         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
282                 'H':'hour', 'M':'minute', 'S':'second'}.items():
283             if info[group] is not None:
284                 setattr(self, attr, int(info[group]))
286         if self.week:
287             self.day = self.day + self.week*7
289         if info['s'] is not None:
290             self.sign = {'+':1, '-':-1}[info['s']]
292     def __repr__(self):
293         return '<Interval %s>'%self.__str__()
295     def pretty(self, threshold=('d', 5)):
296         ''' print up the date date using one of these nice formats..
297             < 1 minute
298             < 15 minutes
299             < 30 minutes
300             < 1 hour
301             < 12 hours
302             < 1 day
303             otherwise, return None (so a full date may be displayed)
304         '''
305         if self.year or self.month or self.day > 5:
306             return None
307         if self.day > 1:
308             return '%s days'%self.day
309         if self.day == 1 or self.hour > 12:
310             return 'yesterday'
311         if self.hour > 1:
312             return '%s hours'%self.hour
313         if self.hour == 1:
314             if self.minute < 15:
315                 return 'an hour'
316             quart = self.minute/15
317             if quart == 2:
318                 return '1 1/2 hours'
319             return '1 %s/4 hours'%quart
320         if self.minute < 1:
321             return 'just now'
322         if self.minute == 1:
323             return '1 minute'
324         if self.minute < 15:
325             return '%s minutes'%self.minute
326         quart = self.minute/15
327         if quart == 2:
328             return '1/2 an hour'
329         return '%s/4 hour'%quart
331     def get_tuple(self):
332         return (self.year, self.month, self.day, self.hour, self.minute,
333             self.second)
336 def test():
337     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
338     for interval in intervals:
339         print '>>> Interval("%s")'%interval
340         print `Interval(interval)`
342     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
343         "08-13.22:13", "14:25")
344     for date in dates:
345         print '>>> Date("%s")'%date
346         print `Date(date)`
348     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
349     for date, interval in sums:
350         print '>>> Date("%s") + Interval("%s")'%(date, interval)
351         print `Date(date) + Interval(interval)`
353 if __name__ == '__main__':
354     test()
357 # $Log: not supported by cvs2svn $
358 # Revision 1.5  2001/07/29 07:01:39  richard
359 # Added vim command to all source so that we don't get no steenkin' tabs :)
361 # Revision 1.4  2001/07/25 04:09:34  richard
362 # Fixed offset handling (shoulda read the spec a little better)
364 # Revision 1.3  2001/07/23 07:56:05  richard
365 # Storing only marshallable data in the db - no nasty pickled class references.
367 # Revision 1.2  2001/07/22 12:09:32  richard
368 # Final commit of Grande Splite
370 # Revision 1.1  2001/07/22 11:58:35  richard
371 # More Grande Splite
374 # vim: set filetype=python ts=4 sw=4 et si