Code

stupid typo
[roundup.git] / roundup / date.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: date.py,v 1.10 2001-08-07 00:24:42 richard Exp $
20 import time, re, calendar
22 class Date:
23     '''
24     As strings, date-and-time stamps are specified with the date in
25     international standard format (yyyy-mm-dd) joined to the time
26     (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
27     and are fairly readable when printed. An example of a valid stamp is
28     "2000-06-24.13:03:59". We'll call this the "full date format". When
29     Timestamp objects are printed as strings, they appear in the full date
30     format with the time always given in GMT. The full date format is
31     always exactly 19 characters long. 
33     For user input, some partial forms are also permitted: the whole time
34     or just the seconds may be omitted; and the whole date may be omitted
35     or just the year may be omitted. If the time is given, the time is
36     interpreted in the user's local time zone. The Date constructor takes
37     care of these conversions. In the following examples, suppose that yyyy
38     is the current year, mm is the current month, and dd is the current day
39     of the month; and suppose that the user is on Eastern Standard Time.
41       "2000-04-17" means <Date 2000-04-17.00:00:00>
42       "01-25" means <Date yyyy-01-25.00:00:00>
43       "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
44       "08-13.22:13" means <Date yyyy-08-14.03:13:00>
45       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
46       "14:25" means <Date yyyy-mm-dd.19:25:00>
47       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
48       "." means "right now"
50     The Date class should understand simple date expressions of the form
51     stamp + interval and stamp - interval. When adding or subtracting
52     intervals involving months or years, the components are handled
53     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
54     first add one month to get 2000-07-25, then add 10 days to get
55     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
56     or 41 days).
58     Example usage:
59         >>> Date(".")
60         <Date 2000-06-26.00:34:02>
61         >>> _.local(-5)
62         "2000-06-25.19:34:02"
63         >>> Date(". + 2d")
64         <Date 2000-06-28.00:34:02>
65         >>> Date("1997-04-17", -5)
66         <Date 1997-04-17.00:00:00>
67         >>> Date("01-25", -5)
68         <Date 2000-01-25.00:00:00>
69         >>> Date("08-13.22:13", -5)
70         <Date 2000-08-14.03:13:00>
71         >>> Date("14:25", -5)
72         <Date 2000-06-25.19:25:00>
73     '''
74     isDate = 1
76     def __init__(self, spec='.', offset=0):
77         """Construct a date given a specification and a time zone offset.
79           'spec' is a full date or a partial form, with an optional
80                  added or subtracted interval. Or a date 9-tuple.
81         'offset' is the local time zone offset from GMT in hours.
82         """
83         if type(spec) == type(''):
84             self.set(spec, offset=offset)
85         else:
86             y,m,d,H,M,S,x,x,x = spec
87             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
88             self.year, self.month, self.day, self.hour, self.minute, \
89                 self.second, x, x, x = time.gmtime(ts)
91     def applyInterval(self, interval):
92         ''' Apply the interval to this date
93         '''
94         t = (self.year + interval.year,
95              self.month + interval.month,
96              self.day + interval.day,
97              self.hour + interval.hour,
98              self.minute + interval.minute,
99              self.second + interval.second, 0, 0, 0)
100         self.year, self.month, self.day, self.hour, self.minute, \
101             self.second, x, x, x = time.gmtime(calendar.timegm(t))
103     def __add__(self, other):
104         """Add an interval to this date to produce another date."""
105         t = (self.year + other.sign * other.year,
106             self.month + other.sign * other.month,
107             self.day + other.sign * other.day,
108             self.hour + other.sign * other.hour,
109             self.minute + other.sign * other.minute,
110             self.second + other.sign * other.second, 0, 0, 0)
111         return Date(time.gmtime(calendar.timegm(t)))
113     # XXX deviates from spec to allow subtraction of dates as well
114     def __sub__(self, other):
115         """ Subtract:
116              1. an interval from this date to produce another date.
117              2. a date from this date to produce an interval.
118         """
119         if other.isDate:
120             # TODO this code will fall over laughing if the dates cross
121             # leap years, phases of the moon, ....
122             a = calendar.timegm((self.year, self.month, self.day, self.hour,
123                 self.minute, self.second, 0, 0, 0))
124             b = calendar.timegm((other.year, other.month, other.day, other.hour,
125                 other.minute, other.second, 0, 0, 0))
126             diff = a - b
127             if diff < 0:
128                 sign = -1
129                 diff = -diff
130             else:
131                 sign = 1
132             S = diff%60
133             M = (diff/60)%60
134             H = (diff/(60*60))%60
135             if H>1: S = 0
136             d = (diff/(24*60*60))%30
137             if d>1: H = S = M = 0
138             m = (diff/(30*24*60*60))%12
139             if m>1: H = S = M = 0
140             y = (diff/(365*24*60*60))
141             if y>1: d = H = S = M = 0
142             return Interval((y, m, d, H, M, S), sign=sign)
143         t = (self.year - other.sign * other.year,
144              self.month - other.sign * other.month,
145              self.day - other.sign * other.day,
146              self.hour - other.sign * other.hour,
147              self.minute - other.sign * other.minute,
148              self.second - other.sign * other.second, 0, 0, 0)
149         return Date(time.gmtime(calendar.timegm(t)))
151     def __cmp__(self, other):
152         """Compare this date to another date."""
153         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
154             r = cmp(getattr(self, attr), getattr(other, attr))
155             if r: return r
156         return 0
158     def __str__(self):
159         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
160         return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
161             self.hour, self.minute, self.second)
163     def pretty(self):
164         ''' print up the date date using a pretty format...
165         '''
166         return time.strftime('%e %B %Y', (self.year, self.month,
167             self.day, self.hour, self.minute, self.second, 0, 0, 0))
169     def set(self, spec, offset=0, date_re=re.compile(r'''
170               (((?P<y>\d\d\d\d)-)?((?P<m>\d\d)-(?P<d>\d\d))?)? # yyyy-mm-dd
171               (?P<n>\.)?                                       # .
172               (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?    # hh:mm:ss
173               (?P<o>.+)?                                       # offset
174               ''', re.VERBOSE)):
175         ''' set the date to the value in spec
176         '''
177         m = date_re.match(spec)
178         if not m:
179             raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]'
180         info = m.groupdict()
182         # get the current date/time using the offset
183         y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
185         # override year, month, day parts
186         if info['m'] is not None and info['d'] is not None:
187             m = int(info['m'])
188             d = int(info['d'])
189             if info['y'] is not None: y = int(info['y'])
190             H = M = S = 0
192         # override hour, minute, second parts
193         if info['H'] is not None and info['M'] is not None:
194             H = int(info['H']) - offset
195             M = int(info['M'])
196             S = 0
197             if info['S'] is not None: S = int(info['S'])
199         # now handle the adjustment of hour
200         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
201         self.year, self.month, self.day, self.hour, self.minute, \
202             self.second, x, x, x = time.gmtime(ts)
204         if info['o']:
205             self.applyInterval(Interval(info['o']))
207     def __repr__(self):
208         return '<Date %s>'%self.__str__()
210     def local(self, offset):
211         """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
212         t = (self.year, self.month, self.day, self.hour + offset, self.minute,
213              self.second, 0, 0, 0)
214         self.year, self.month, self.day, self.hour, self.minute, \
215             self.second, x, x, x = time.gmtime(calendar.timegm(t))
217     def get_tuple(self):
218         return (self.year, self.month, self.day, self.hour, self.minute,
219             self.second, 0, 0, 0)
221 class Interval:
222     '''
223     Date intervals are specified using the suffixes "y", "m", and "d". The
224     suffix "w" (for "week") means 7 days. Time intervals are specified in
225     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
226     may not).
228       "3y" means three years
229       "2y 1m" means two years and one month
230       "1m 25d" means one month and 25 days
231       "2w 3d" means two weeks and three days
232       "1d 2:50" means one day, two hours, and 50 minutes
233       "14:00" means 14 hours
234       "0:04:33" means four minutes and 33 seconds
236     Example usage:
237         >>> Interval("  3w  1  d  2:00")
238         <Interval 22d 2:00>
239         >>> Date(". + 2d") - Interval("3w")
240         <Date 2000-06-07.00:34:02>
241     '''
242     isInterval = 1
244     def __init__(self, spec, sign=1):
245         """Construct an interval given a specification."""
246         if type(spec) == type(''):
247             self.set(spec)
248         else:
249             self.sign = sign
250             self.year, self.month, self.day, self.hour, self.minute, \
251                 self.second = spec
253     def __cmp__(self, other):
254         """Compare this interval to another interval."""
255         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
256             r = cmp(getattr(self, attr), getattr(other, attr))
257             if r: return r
258         return 0
259         
260     def __str__(self):
261         """Return this interval as a string."""
262         sign = {1:'+', -1:'-'}[self.sign]
263         l = [sign]
264         if self.year: l.append('%sy'%self.year)
265         if self.month: l.append('%sm'%self.month)
266         if self.day: l.append('%sd'%self.day)
267         if self.second:
268             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
269         elif self.hour or self.minute:
270             l.append('%d:%02d'%(self.hour, self.minute))
271         return ' '.join(l)
273     def set(self, spec, interval_re = re.compile('''
274             \s*
275             (?P<s>[-+])?         # + or -
276             \s*
277             ((?P<y>\d+\s*)y)?    # year
278             \s*
279             ((?P<m>\d+\s*)m)?    # month
280             \s*
281             ((?P<w>\d+\s*)w)?    # week
282             \s*
283             ((?P<d>\d+\s*)d)?    # day
284             \s*
285             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?   # time
286             \s*
287             ''', re.VERBOSE)):
288         ''' set the date to the value in spec
289         '''
290         self.year = self.month = self.week = self.day = self.hour = \
291             self.minute = self.second = 0
292         self.sign = 1
293         m = interval_re.match(spec)
294         if not m:
295             raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]'
297         info = m.groupdict()
298         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
299                 'H':'hour', 'M':'minute', 'S':'second'}.items():
300             if info[group] is not None:
301                 setattr(self, attr, int(info[group]))
303         if self.week:
304             self.day = self.day + self.week*7
306         if info['s'] is not None:
307             self.sign = {'+':1, '-':-1}[info['s']]
309     def __repr__(self):
310         return '<Interval %s>'%self.__str__()
312     def pretty(self, threshold=('d', 5)):
313         ''' print up the date date using one of these nice formats..
314             < 1 minute
315             < 15 minutes
316             < 30 minutes
317             < 1 hour
318             < 12 hours
319             < 1 day
320             otherwise, return None (so a full date may be displayed)
321         '''
322         if self.year or self.month > 2:
323             return None
324         if self.month:
325             days = (self.month * 30) + self.day
326             if days > 28:
327                 return '%s months'%int(days/30)
328             else:
329                 return '%s weeks'%int(days/7)
330         if self.day > 7:
331             return '%s weeks'%self.day
332         if self.day > 1:
333             return '%s days'%self.day
334         if self.day == 1 or self.hour > 12:
335             return 'yesterday'
336         if self.hour > 1:
337             return '%s hours'%self.hour
338         if self.hour == 1:
339             if self.minute < 15:
340                 return 'an hour'
341             quart = self.minute/15
342             if quart == 2:
343                 return '1 1/2 hours'
344             return '1 %s/4 hours'%quart
345         if self.minute < 1:
346             return 'just now'
347         if self.minute == 1:
348             return '1 minute'
349         if self.minute < 15:
350             return '%s minutes'%self.minute
351         quart = int(self.minute/15)
352         if quart == 2:
353             return '1/2 an hour'
354         return '%s/4 hour'%quart
356     def get_tuple(self):
357         return (self.year, self.month, self.day, self.hour, self.minute,
358             self.second)
361 def test():
362     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
363     for interval in intervals:
364         print '>>> Interval("%s")'%interval
365         print `Interval(interval)`
367     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
368         "08-13.22:13", "14:25")
369     for date in dates:
370         print '>>> Date("%s")'%date
371         print `Date(date)`
373     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
374     for date, interval in sums:
375         print '>>> Date("%s") + Interval("%s")'%(date, interval)
376         print `Date(date) + Interval(interval)`
378 if __name__ == '__main__':
379     test()
382 # $Log: not supported by cvs2svn $
383 # Revision 1.9  2001/08/07 00:15:51  richard
384 # Added the copyright/license notice to (nearly) all files at request of
385 # Bizar Software.
387 # Revision 1.8  2001/08/05 07:46:12  richard
388 # Changed date.Date to use regular string formatting instead of strftime -
389 # win32 seems to have problems with %T and no hour... or something...
391 # Revision 1.7  2001/08/02 00:27:04  richard
392 # Extended the range of intervals that are pretty-printed before actual dates
393 # are displayed.
395 # Revision 1.6  2001/07/31 09:54:18  richard
396 # Fixed the 2.1-specific gmtime() (no arg) call in roundup.date. (Paul Wright)
398 # Revision 1.5  2001/07/29 07:01:39  richard
399 # Added vim command to all source so that we don't get no steenkin' tabs :)
401 # Revision 1.4  2001/07/25 04:09:34  richard
402 # Fixed offset handling (shoulda read the spec a little better)
404 # Revision 1.3  2001/07/23 07:56:05  richard
405 # Storing only marshallable data in the db - no nasty pickled class references.
407 # Revision 1.2  2001/07/22 12:09:32  richard
408 # Final commit of Grande Splite
410 # Revision 1.1  2001/07/22 11:58:35  richard
411 # More Grande Splite
414 # vim: set filetype=python ts=4 sw=4 et si