Code

add more doc to date.py
[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.64 2004-04-06 21:56:10 richard Exp $
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
24 import time, re, calendar, types
25 from i18n import _
27 def _add_granularity(src, order, value = 1):
28     '''Increment first non-None value in src dictionary ordered by 'order'
29     parameter
30     '''
31     for gran in order:
32         if src[gran]:
33             src[gran] = int(src[gran]) + value
34             break
36 class Date:
37     '''
38     As strings, date-and-time stamps are specified with the date in
39     international standard format (yyyy-mm-dd) joined to the time
40     (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
41     and are fairly readable when printed. An example of a valid stamp is
42     "2000-06-24.13:03:59". We'll call this the "full date format". When
43     Timestamp objects are printed as strings, they appear in the full date
44     format with the time always given in GMT. The full date format is
45     always exactly 19 characters long. 
47     For user input, some partial forms are also permitted: the whole time
48     or just the seconds may be omitted; and the whole date may be omitted
49     or just the year may be omitted. If the time is given, the time is
50     interpreted in the user's local time zone. The Date constructor takes
51     care of these conversions. In the following examples, suppose that yyyy
52     is the current year, mm is the current month, and dd is the current day
53     of the month; and suppose that the user is on Eastern Standard Time.
54     Examples::
56       "2000-04-17" means <Date 2000-04-17.00:00:00>
57       "01-25" means <Date yyyy-01-25.00:00:00>
58       "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
59       "08-13.22:13" means <Date yyyy-08-14.03:13:00>
60       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
61       "14:25" means <Date yyyy-mm-dd.19:25:00>
62       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
63       "2003" means <Date 2003-01-01.00:00:00>
64       "2003-06" means <Date 2003-06-01.00:00:00>
65       "." means "right now"
67     The Date class should understand simple date expressions of the form
68     stamp + interval and stamp - interval. When adding or subtracting
69     intervals involving months or years, the components are handled
70     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
71     first add one month to get 2000-07-25, then add 10 days to get
72     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
73     or 41 days).  Example usage::
75         >>> Date(".")
76         <Date 2000-06-26.00:34:02>
77         >>> _.local(-5)
78         "2000-06-25.19:34:02"
79         >>> Date(". + 2d")
80         <Date 2000-06-28.00:34:02>
81         >>> Date("1997-04-17", -5)
82         <Date 1997-04-17.00:00:00>
83         >>> Date("01-25", -5)
84         <Date 2000-01-25.00:00:00>
85         >>> Date("08-13.22:13", -5)
86         <Date 2000-08-14.03:13:00>
87         >>> Date("14:25", -5)
88         <Date 2000-06-25.19:25:00>
90     The date format 'yyyymmddHHMMSS' (year, month, day, hour,
91     minute, second) is the serialisation format returned by the serialise()
92     method, and is accepted as an argument on instatiation.
94     The date class handles basic arithmetic::
96         >>> d1=Date('.')
97         >>> d1
98         <Date 2004-04-06.22:04:20.766830>
99         >>> d2=Date('2003-07-01')
100         >>> d2
101         <Date 2003-07-01.00:00:0.000000>
102         >>> d1-d2
103         <Interval + 280d 22:04:20>
104         >>> i1=_
105         >>> d2+i1
106         <Date 2004-04-06.22:04:20.000000>
107         >>> d1-i1
108         <Date 2003-07-01.00:00:0.000000>
109     '''
110     
111     def __init__(self, spec='.', offset=0, add_granularity=0):
112         """Construct a date given a specification and a time zone offset.
114         'spec'
115            is a full date or a partial form, with an optional added or
116            subtracted interval. Or a date 9-tuple.
117         'offset'
118            is the local time zone offset from GMT in hours.
119         """
120         if type(spec) == type(''):
121             self.set(spec, offset=offset, add_granularity=add_granularity)
122             return
123         elif hasattr(spec, 'tuple'):
124             spec = spec.tuple()
125         try:
126             y,m,d,H,M,S,x,x,x = spec
127             frac = S - int(S)
128             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
129             self.year, self.month, self.day, self.hour, self.minute, \
130                 self.second, x, x, x = time.gmtime(ts)
131             # we lost the fractional part
132             self.second = self.second + frac
133         except:
134             raise ValueError, 'Unknown spec %r'%spec
136     usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS.SSS][offset]'
137     def set(self, spec, offset=0, date_re=re.compile(r'''
138             ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
139             |(?P<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
140             (?P<n>\.)?                                     # .
141             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d(\.\d+)?))?)?  # hh:mm:ss
142             (?P<o>.+)?                                     # offset
143             ''', re.VERBOSE), serialised_re=re.compile(r'''
144             (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d(\.\d+)?)
145             ''', re.VERBOSE), add_granularity=0):
146         ''' set the date to the value in spec
147         '''
149         m = serialised_re.match(spec)
150         if m is not None:
151             # we're serialised - easy!
152             g = m.groups()
153             (self.year, self.month, self.day, self.hour, self.minute) = \
154                 map(int, g[:5])
155             self.second = float(g[5])
156             return
158         # not serialised data, try usual format
159         m = date_re.match(spec)
160         if m is None:
161             raise ValueError, _('Not a date spec: %s' % self.usagespec)
163         info = m.groupdict()
165         if add_granularity:
166             _add_granularity(info, 'SMHdmyab')
168         # get the current date as our default
169         ts = time.time()
170         frac = ts - int(ts)
171         y,m,d,H,M,S,x,x,x = time.gmtime(ts)
172         # gmtime loses the fractional seconds 
173         S = S + frac
175         if info['y'] is not None or info['a'] is not None:
176             if info['y'] is not None:
177                 y = int(info['y'])
178                 m,d = (1,1)
179                 if info['m'] is not None:
180                     m = int(info['m'])
181                     if info['d'] is not None:
182                         d = int(info['d'])
183             if info['a'] is not None:
184                 m = int(info['a'])
185                 d = int(info['b'])
186             H = -offset
187             M = S = 0
189         # override hour, minute, second parts
190         if info['H'] is not None and info['M'] is not None:
191             H = int(info['H']) - offset
192             M = int(info['M'])
193             S = 0
194             if info['S'] is not None:
195                 S = float(info['S'])
197         if add_granularity:
198             S = S - 1
199         
200         # now handle the adjustment of hour
201         frac = S - int(S)
202         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
203         self.year, self.month, self.day, self.hour, self.minute, \
204             self.second, x, x, x = time.gmtime(ts)
205         # we lost the fractional part along the way
206         self.second = self.second + frac
208         if info.get('o', None):
209             try:
210                 self.applyInterval(Interval(info['o'], allowdate=0))
211             except ValueError:
212                 raise ValueError, _('%r not a date spec (%s)')%(spec,
213                     self.usagespec)
215     def addInterval(self, interval):
216         ''' Add the interval to this date, returning the date tuple
217         '''
218         # do the basic calc
219         sign = interval.sign
220         year = self.year + sign * interval.year
221         month = self.month + sign * interval.month
222         day = self.day + sign * interval.day
223         hour = self.hour + sign * interval.hour
224         minute = self.minute + sign * interval.minute
225         # Intervals work on whole seconds
226         second = int(self.second) + sign * interval.second
228         # now cope with under- and over-flow
229         # first do the time
230         while (second < 0 or second > 59 or minute < 0 or minute > 59 or
231                 hour < 0 or hour > 23):
232             if second < 0: minute -= 1; second += 60
233             elif second > 59: minute += 1; second -= 60
234             if minute < 0: hour -= 1; minute += 60
235             elif minute > 59: hour += 1; minute -= 60
236             if hour < 0: day -= 1; hour += 24
237             elif hour > 23: day += 1; hour -= 24
239         # fix up the month so we're within range
240         while month < 1 or month > 12:
241             if month < 1: year -= 1; month += 12
242             if month > 12: year += 1; month -= 12
244         # now do the days, now that we know what month we're in
245         def get_mdays(year, month):
246             if month == 2 and calendar.isleap(year): return 29
247             else: return calendar.mdays[month]
249         while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
250             # now to day under/over
251             if day < 1: 
252                 # When going backwards, decrement month, then increment days
253                 month -= 1
254                 day += get_mdays(year,month)
255             elif day > get_mdays(year,month): 
256                 # When going forwards, decrement days, then increment month
257                 day -= get_mdays(year,month)
258                 month += 1
260             # possibly fix up the month so we're within range
261             while month < 1 or month > 12:
262                 if month < 1: year -= 1; month += 12 ; day += 31
263                 if month > 12: year += 1; month -= 12
265         return (year, month, day, hour, minute, second, 0, 0, 0)
267     def differenceDate(self, other):
268         "Return the difference between this date and another date"
270     def applyInterval(self, interval):
271         ''' Apply the interval to this date
272         '''
273         self.year, self.month, self.day, self.hour, self.minute, \
274             self.second, x, x, x = self.addInterval(interval)
276     def __add__(self, interval):
277         """Add an interval to this date to produce another date.
278         """
279         return Date(self.addInterval(interval))
281     # deviates from spec to allow subtraction of dates as well
282     def __sub__(self, other):
283         """ Subtract:
284              1. an interval from this date to produce another date.
285              2. a date from this date to produce an interval.
286         """
287         if isinstance(other, Interval):
288             other = Interval(other.get_tuple())
289             other.sign *= -1
290             return self.__add__(other)
292         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
294         return self.dateDelta(other)
296     def dateDelta(self, other):
297         """ Produce an Interval of the difference between this date
298             and another date. Only returns days:hours:minutes:seconds.
299         """
300         # Returning intervals larger than a day is almost
301         # impossible - months, years, weeks, are all so imprecise.
302         a = calendar.timegm((self.year, self.month, self.day, self.hour,
303             self.minute, self.second, 0, 0, 0))
304         b = calendar.timegm((other.year, other.month, other.day,
305             other.hour, other.minute, other.second, 0, 0, 0))
306         # intervals work in whole seconds
307         diff = int(a - b)
308         if diff > 0:
309             sign = 1
310         else:
311             sign = -1
312             diff = -diff
313         S = diff%60
314         M = (diff/60)%60
315         H = (diff/(60*60))%24
316         d = diff/(24*60*60)
317         return Interval((0, 0, d, H, M, S), sign=sign)
319     def __cmp__(self, other, int_seconds=0):
320         """Compare this date to another date."""
321         if other is None:
322             return 1
323         for attr in ('year', 'month', 'day', 'hour', 'minute'):
324             if not hasattr(other, attr):
325                 return 1
326             r = cmp(getattr(self, attr), getattr(other, attr))
327             if r: return r
328         if not hasattr(other, 'second'):
329             return 1
330         if int_seconds:
331             return cmp(int(self.second), int(other.second))
332         return cmp(self.second, other.second)
334     def __str__(self):
335         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
336         return self.formal()
338     def formal(self, sep='.', sec='%02d'):
339         f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
340         return f%(self.year, self.month, self.day, self.hour, self.minute,
341             self.second)
343     def pretty(self, format='%d %B %Y'):
344         ''' print up the date date using a pretty format...
346             Note that if the day is zero, and the day appears first in the
347             format, then the day number will be removed from output.
348         '''
349         str = time.strftime(format, (self.year, self.month, self.day,
350             self.hour, self.minute, self.second, 0, 0, 0))
351         # handle zero day by removing it
352         if format.startswith('%d') and str[0] == '0':
353             return ' ' + str[1:]
354         return str
356     def __repr__(self):
357         return '<Date %s>'%self.formal(sec='%f')
359     def local(self, offset):
360         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
361         """
362         return Date((self.year, self.month, self.day, self.hour + offset,
363             self.minute, self.second, 0, 0, 0))
365     def get_tuple(self):
366         return (self.year, self.month, self.day, self.hour, self.minute,
367             self.second, 0, 0, 0)
369     def serialise(self):
370         return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
371             self.day, self.hour, self.minute, self.second)
373     def timestamp(self):
374         ''' return a UNIX timestamp for this date '''
375         frac = self.second - int(self.second)
376         ts = calendar.timegm((self.year, self.month, self.day, self.hour,
377             self.minute, self.second, 0, 0, 0))
378         # we lose the fractional part
379         return ts + frac
381 class Interval:
382     '''
383     Date intervals are specified using the suffixes "y", "m", and "d". The
384     suffix "w" (for "week") means 7 days. Time intervals are specified in
385     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
386     may not).
388       "3y" means three years
389       "2y 1m" means two years and one month
390       "1m 25d" means one month and 25 days
391       "2w 3d" means two weeks and three days
392       "1d 2:50" means one day, two hours, and 50 minutes
393       "14:00" means 14 hours
394       "0:04:33" means four minutes and 33 seconds
396     Example usage:
397         >>> Interval("  3w  1  d  2:00")
398         <Interval + 22d 2:00>
399         >>> Date(". + 2d") + Interval("- 3w")
400         <Date 2000-06-07.00:34:02>
401         >>> Interval('1:59:59') + Interval('00:00:01')
402         <Interval + 2:00>
403         >>> Interval('2:00') + Interval('- 00:00:01')
404         <Interval + 1:59:59>
405         >>> Interval('1y')/2
406         <Interval + 6m>
407         >>> Interval('1:00')/2
408         <Interval + 0:30>
409         >>> Interval('2003-03-18')
410         <Interval + [number of days between now and 2003-03-18]>
411         >>> Interval('-4d 2003-03-18')
412         <Interval + [number of days between now and 2003-03-14]>
414     Interval arithmetic is handled in a couple of special ways, trying
415     to cater for the most common cases. Fundamentally, Intervals which
416     have both date and time parts will result in strange results in
417     arithmetic - because of the impossibility of handling day->month->year
418     over- and under-flows. Intervals may also be divided by some number.
420     Intervals are added to Dates in order of:
421        seconds, minutes, hours, years, months, days
423     Calculations involving months (eg '+2m') have no effect on days - only
424     days (or over/underflow from hours/mins/secs) will do that, and
425     days-per-month and leap years are accounted for. Leap seconds are not.
427     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
428     minute, second) is the serialisation format returned by the serialise()
429     method, and is accepted as an argument on instatiation.
431     TODO: more examples, showing the order of addition operation
432     '''
433     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
434         """Construct an interval given a specification."""
435         if type(spec) == type(''):
436             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
437         else:
438             if len(spec) == 7:
439                 self.sign, self.year, self.month, self.day, self.hour, \
440                     self.minute, self.second = spec
441                 self.second = int(self.second)
442             else:
443                 # old, buggy spec form
444                 self.sign = sign
445                 self.year, self.month, self.day, self.hour, self.minute, \
446                     self.second = spec
447                 self.second = int(self.second)
449     def set(self, spec, allowdate=1, interval_re=re.compile('''
450             \s*(?P<s>[-+])?         # + or -
451             \s*((?P<y>\d+\s*)y)?    # year
452             \s*((?P<m>\d+\s*)m)?    # month
453             \s*((?P<w>\d+\s*)w)?    # week
454             \s*((?P<d>\d+\s*)d)?    # day
455             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
456             \s*(?P<D>
457                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
458                  \.?                                       # .
459                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
460                )?''', re.VERBOSE), serialised_re=re.compile('''
461             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
462             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
463             add_granularity=0):
464         ''' set the date to the value in spec
465         '''
466         self.year = self.month = self.week = self.day = self.hour = \
467             self.minute = self.second = 0
468         self.sign = 1
469         m = serialised_re.match(spec)
470         if not m:
471             m = interval_re.match(spec)
472             if not m:
473                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
474                     '[#d] [[[H]H:MM]:SS] [date spec]')
475         else:
476             allowdate = 0
478         # pull out all the info specified
479         info = m.groupdict()
480         if add_granularity:
481             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
483         valid = 0
484         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
485                 'H':'hour', 'M':'minute', 'S':'second'}.items():
486             if info.get(group, None) is not None:
487                 valid = 1
488                 setattr(self, attr, int(info[group]))
490         # make sure it's valid
491         if not valid and not info['D']:
492             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
493                 '[#d] [[[H]H:MM]:SS]')
495         if self.week:
496             self.day = self.day + self.week*7
498         if info['s'] is not None:
499             self.sign = {'+':1, '-':-1}[info['s']]
501         # use a date spec if one is given
502         if allowdate and info['D'] is not None:
503             now = Date('.')
504             date = Date(info['D'])
505             # if no time part was specified, nuke it in the "now" date
506             if not date.hour or date.minute or date.second:
507                 now.hour = now.minute = now.second = 0
508             if date != now:
509                 y = now - (date + self)
510                 self.__init__(y.get_tuple())
512     def __cmp__(self, other):
513         """Compare this interval to another interval."""
514         if other is None:
515             # we are always larger than None
516             return 1
517         for attr in 'sign year month day hour minute second'.split():
518             r = cmp(getattr(self, attr), getattr(other, attr))
519             if r:
520                 return r
521         return 0
523     def __str__(self):
524         """Return this interval as a string."""
525         l = []
526         if self.year: l.append('%sy'%self.year)
527         if self.month: l.append('%sm'%self.month)
528         if self.day: l.append('%sd'%self.day)
529         if self.second:
530             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
531         elif self.hour or self.minute:
532             l.append('%d:%02d'%(self.hour, self.minute))
533         if l:
534             l.insert(0, {1:'+', -1:'-'}[self.sign])
535         return ' '.join(l)
537     def __add__(self, other):
538         if isinstance(other, Date):
539             # the other is a Date - produce a Date
540             return Date(other.addInterval(self))
541         elif isinstance(other, Interval):
542             # add the other Interval to this one
543             a = self.get_tuple()
544             as = a[0]
545             b = other.get_tuple()
546             bs = b[0]
547             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
548             i.insert(0, 1)
549             i = fixTimeOverflow(i)
550             return Interval(i)
551         # nope, no idea what to do with this other...
552         raise TypeError, "Can't add %r"%other
554     def __sub__(self, other):
555         if isinstance(other, Date):
556             # the other is a Date - produce a Date
557             interval = Interval(self.get_tuple())
558             interval.sign *= -1
559             return Date(other.addInterval(interval))
560         elif isinstance(other, Interval):
561             # add the other Interval to this one
562             a = self.get_tuple()
563             as = a[0]
564             b = other.get_tuple()
565             bs = b[0]
566             i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
567             i.insert(0, 1)
568             i = fixTimeOverflow(i)
569             return Interval(i)
570         # nope, no idea what to do with this other...
571         raise TypeError, "Can't add %r"%other
573     def __div__(self, other):
574         """ Divide this interval by an int value.
576             Can't divide years and months sensibly in the _same_
577             calculation as days/time, so raise an error in that situation.
578         """
579         try:
580             other = float(other)
581         except TypeError:
582             raise ValueError, "Can only divide Intervals by numbers"
584         y, m, d, H, M, S = (self.year, self.month, self.day,
585             self.hour, self.minute, self.second)
586         if y or m:
587             if d or H or M or S:
588                 raise ValueError, "Can't divide Interval with date and time"
589             months = self.year*12 + self.month
590             months *= self.sign
592             months = int(months/other)
594             sign = months<0 and -1 or 1
595             m = months%12
596             y = months / 12
597             return Interval((sign, y, m, 0, 0, 0, 0))
599         else:
600             # handle a day/time division
601             seconds = S + M*60 + H*60*60 + d*60*60*24
602             seconds *= self.sign
604             seconds = int(seconds/other)
606             sign = seconds<0 and -1 or 1
607             seconds *= sign
608             S = seconds%60
609             seconds /= 60
610             M = seconds%60
611             seconds /= 60
612             H = seconds%24
613             d = seconds / 24
614             return Interval((sign, 0, 0, d, H, M, S))
616     def __repr__(self):
617         return '<Interval %s>'%self.__str__()
619     def pretty(self):
620         ''' print up the date date using one of these nice formats..
621         '''
622         if self.year:
623             if self.year == 1:
624                 s = _('1 year')
625             else:
626                 s = _('%(number)s years')%{'number': self.year}
627         elif self.month or self.day > 13:
628             days = (self.month * 30) + self.day
629             if days > 28:
630                 if int(days/30) > 1:
631                     s = _('%(number)s months')%{'number': int(days/30)}
632                 else:
633                     s = _('1 month')
634             else:
635                 s = _('%(number)s weeks')%{'number': int(days/7)}
636         elif self.day > 7:
637             s = _('1 week')
638         elif self.day > 1:
639             s = _('%(number)s days')%{'number': self.day}
640         elif self.day == 1 or self.hour > 12:
641             if self.sign > 0:
642                 return _('tomorrow')
643             else:
644                 return _('yesterday')
645         elif self.hour > 1:
646             s = _('%(number)s hours')%{'number': self.hour}
647         elif self.hour == 1:
648             if self.minute < 15:
649                 s = _('an hour')
650             elif self.minute/15 == 2:
651                 s = _('1 1/2 hours')
652             else:
653                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
654         elif self.minute < 1:
655             if self.sign > 0:
656                 return _('in a moment')
657             else:
658                 return _('just now')
659         elif self.minute == 1:
660             s = _('1 minute')
661         elif self.minute < 15:
662             s = _('%(number)s minutes')%{'number': self.minute}
663         elif int(self.minute/15) == 2:
664             s = _('1/2 an hour')
665         else:
666             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
667         if self.sign < 0: 
668             s = s + _(' ago')
669         else:
670             s = _('in ') + s
671         return s
673     def get_tuple(self):
674         return (self.sign, self.year, self.month, self.day, self.hour,
675             self.minute, self.second)
677     def serialise(self):
678         sign = self.sign > 0 and '+' or '-'
679         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
680             self.day, self.hour, self.minute, self.second)
682 def fixTimeOverflow(time):
683     """ Handle the overflow in the time portion (H, M, S) of "time":
684             (sign, y,m,d,H,M,S)
686         Overflow and underflow will at most affect the _days_ portion of
687         the date. We do not overflow days to months as we don't know _how_
688         to, generally.
689     """
690     # XXX we could conceivably use this function for handling regular dates
691     # XXX too - we just need to interrogate the month/year for the day
692     # XXX overflow...
694     sign, y, m, d, H, M, S = time
695     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
696     if seconds:
697         sign = seconds<0 and -1 or 1
698         seconds *= sign
699         S = seconds%60
700         seconds /= 60
701         M = seconds%60
702         seconds /= 60
703         H = seconds%24
704         d = seconds / 24
705     else:
706         months = y*12 + m
707         sign = months<0 and -1 or 1
708         months *= sign
709         m = months%12
710         y = months/12
712     return (sign, y, m, d, H, M, S)
714 class Range:
715     """Represents range between two values
716     Ranges can be created using one of theese two alternative syntaxes:
717         
718     1. Native english syntax::
720             [[From] <value>][ To <value>]
722        Keywords "From" and "To" are case insensitive. Keyword "From" is
723        optional.
725     2. "Geek" syntax::
727           [<value>][; <value>]
729     Either first or second <value> can be omitted in both syntaxes.
731     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003)::
733         >>> Range("from 2-12 to 4-2")
734         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
735         
736         >>> Range("18:00 TO +2m")
737         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
738         
739         >>> Range("12:00")
740         <Range from 2003-03-08.12:00:00 to None>
741         
742         >>> Range("tO +3d")
743         <Range from None to 2003-03-11.20:07:48>
744         
745         >>> Range("2002-11-10; 2002-12-12")
746         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
747         
748         >>> Range("; 20:00 +1d")
749         <Range from None to 2003-03-09.20:00:00>
751     """
752     def __init__(self, spec, Type, allow_granularity=1, **params):
753         """Initializes Range of type <Type> from given <spec> string.
754         
755         Sets two properties - from_value and to_value. None assigned to any of
756         this properties means "infinitum" (-infinitum to from_value and
757         +infinitum to to_value)
759         The Type parameter here should be class itself (e.g. Date), not a
760         class instance.
761         
762         """
763         self.range_type = Type
764         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
765         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
766         # Check which syntax to use
767         if  spec.find(';') == -1:
768             # Native english
769             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
770         else:
771             # Geek
772             mch_range = re.search(re_geek_range, spec.strip())
773         if mch_range:
774             self.from_value, self.to_value = mch_range.groups()
775             if self.from_value:
776                 self.from_value = Type(self.from_value.strip(), **params)
777             if self.to_value:
778                 self.to_value = Type(self.to_value.strip(), **params)
779         else:
780             if allow_granularity:
781                 self.from_value = Type(spec, **params)
782                 self.to_value = Type(spec, add_granularity=1, **params)
783             else:
784                 raise ValueError, "Invalid range"
786     def __str__(self):
787         return "from %s to %s" % (self.from_value, self.to_value)
789     def __repr__(self):
790         return "<Range %s>" % self.__str__()
791  
792 def test_range():
793     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
794         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
795     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
796     for rspec in rspecs:
797         print '>>> Range("%s")' % rspec
798         print `Range(rspec, Date)`
799         print
800     for rspec in rispecs:
801         print '>>> Range("%s")' % rspec
802         print `Range(rspec, Interval)`
803         print
805 def test():
806     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
807     for interval in intervals:
808         print '>>> Interval("%s")'%interval
809         print `Interval(interval)`
811     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
812         "08-13.22:13", "14:25", '2002-12')
813     for date in dates:
814         print '>>> Date("%s")'%date
815         print `Date(date)`
817     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
818     for date, interval in sums:
819         print '>>> Date("%s") + Interval("%s")'%(date, interval)
820         print `Date(date) + Interval(interval)`
822 if __name__ == '__main__':
823     test()
825 # vim: set filetype=python ts=4 sw=4 et si