Code

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