Code

implement differenceDate ;)
[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.65 2004-04-06 22:43:59 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"
269         return self - other
271     def applyInterval(self, interval):
272         ''' Apply the interval to this date
273         '''
274         self.year, self.month, self.day, self.hour, self.minute, \
275             self.second, x, x, x = self.addInterval(interval)
277     def __add__(self, interval):
278         """Add an interval to this date to produce another date.
279         """
280         return Date(self.addInterval(interval))
282     # deviates from spec to allow subtraction of dates as well
283     def __sub__(self, other):
284         """ Subtract:
285              1. an interval from this date to produce another date.
286              2. a date from this date to produce an interval.
287         """
288         if isinstance(other, Interval):
289             other = Interval(other.get_tuple())
290             other.sign *= -1
291             return self.__add__(other)
293         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
295         return self.dateDelta(other)
297     def dateDelta(self, other):
298         """ Produce an Interval of the difference between this date
299             and another date. Only returns days:hours:minutes:seconds.
300         """
301         # Returning intervals larger than a day is almost
302         # impossible - months, years, weeks, are all so imprecise.
303         a = calendar.timegm((self.year, self.month, self.day, self.hour,
304             self.minute, self.second, 0, 0, 0))
305         b = calendar.timegm((other.year, other.month, other.day,
306             other.hour, other.minute, other.second, 0, 0, 0))
307         # intervals work in whole seconds
308         diff = int(a - b)
309         if diff > 0:
310             sign = 1
311         else:
312             sign = -1
313             diff = -diff
314         S = diff%60
315         M = (diff/60)%60
316         H = (diff/(60*60))%24
317         d = diff/(24*60*60)
318         return Interval((0, 0, d, H, M, S), sign=sign)
320     def __cmp__(self, other, int_seconds=0):
321         """Compare this date to another date."""
322         if other is None:
323             return 1
324         for attr in ('year', 'month', 'day', 'hour', 'minute'):
325             if not hasattr(other, attr):
326                 return 1
327             r = cmp(getattr(self, attr), getattr(other, attr))
328             if r: return r
329         if not hasattr(other, 'second'):
330             return 1
331         if int_seconds:
332             return cmp(int(self.second), int(other.second))
333         return cmp(self.second, other.second)
335     def __str__(self):
336         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
337         return self.formal()
339     def formal(self, sep='.', sec='%02d'):
340         f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
341         return f%(self.year, self.month, self.day, self.hour, self.minute,
342             self.second)
344     def pretty(self, format='%d %B %Y'):
345         ''' print up the date date using a pretty format...
347             Note that if the day is zero, and the day appears first in the
348             format, then the day number will be removed from output.
349         '''
350         str = time.strftime(format, (self.year, self.month, self.day,
351             self.hour, self.minute, self.second, 0, 0, 0))
352         # handle zero day by removing it
353         if format.startswith('%d') and str[0] == '0':
354             return ' ' + str[1:]
355         return str
357     def __repr__(self):
358         return '<Date %s>'%self.formal(sec='%f')
360     def local(self, offset):
361         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
362         """
363         return Date((self.year, self.month, self.day, self.hour + offset,
364             self.minute, self.second, 0, 0, 0))
366     def get_tuple(self):
367         return (self.year, self.month, self.day, self.hour, self.minute,
368             self.second, 0, 0, 0)
370     def serialise(self):
371         return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
372             self.day, self.hour, self.minute, self.second)
374     def timestamp(self):
375         ''' return a UNIX timestamp for this date '''
376         frac = self.second - int(self.second)
377         ts = calendar.timegm((self.year, self.month, self.day, self.hour,
378             self.minute, self.second, 0, 0, 0))
379         # we lose the fractional part
380         return ts + frac
382 class Interval:
383     '''
384     Date intervals are specified using the suffixes "y", "m", and "d". The
385     suffix "w" (for "week") means 7 days. Time intervals are specified in
386     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
387     may not).
389       "3y" means three years
390       "2y 1m" means two years and one month
391       "1m 25d" means one month and 25 days
392       "2w 3d" means two weeks and three days
393       "1d 2:50" means one day, two hours, and 50 minutes
394       "14:00" means 14 hours
395       "0:04:33" means four minutes and 33 seconds
397     Example usage:
398         >>> Interval("  3w  1  d  2:00")
399         <Interval + 22d 2:00>
400         >>> Date(". + 2d") + Interval("- 3w")
401         <Date 2000-06-07.00:34:02>
402         >>> Interval('1:59:59') + Interval('00:00:01')
403         <Interval + 2:00>
404         >>> Interval('2:00') + Interval('- 00:00:01')
405         <Interval + 1:59:59>
406         >>> Interval('1y')/2
407         <Interval + 6m>
408         >>> Interval('1:00')/2
409         <Interval + 0:30>
410         >>> Interval('2003-03-18')
411         <Interval + [number of days between now and 2003-03-18]>
412         >>> Interval('-4d 2003-03-18')
413         <Interval + [number of days between now and 2003-03-14]>
415     Interval arithmetic is handled in a couple of special ways, trying
416     to cater for the most common cases. Fundamentally, Intervals which
417     have both date and time parts will result in strange results in
418     arithmetic - because of the impossibility of handling day->month->year
419     over- and under-flows. Intervals may also be divided by some number.
421     Intervals are added to Dates in order of:
422        seconds, minutes, hours, years, months, days
424     Calculations involving months (eg '+2m') have no effect on days - only
425     days (or over/underflow from hours/mins/secs) will do that, and
426     days-per-month and leap years are accounted for. Leap seconds are not.
428     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
429     minute, second) is the serialisation format returned by the serialise()
430     method, and is accepted as an argument on instatiation.
432     TODO: more examples, showing the order of addition operation
433     '''
434     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
435         """Construct an interval given a specification."""
436         if type(spec) == type(''):
437             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
438         else:
439             if len(spec) == 7:
440                 self.sign, self.year, self.month, self.day, self.hour, \
441                     self.minute, self.second = spec
442                 self.second = int(self.second)
443             else:
444                 # old, buggy spec form
445                 self.sign = sign
446                 self.year, self.month, self.day, self.hour, self.minute, \
447                     self.second = spec
448                 self.second = int(self.second)
450     def set(self, spec, allowdate=1, interval_re=re.compile('''
451             \s*(?P<s>[-+])?         # + or -
452             \s*((?P<y>\d+\s*)y)?    # year
453             \s*((?P<m>\d+\s*)m)?    # month
454             \s*((?P<w>\d+\s*)w)?    # week
455             \s*((?P<d>\d+\s*)d)?    # day
456             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
457             \s*(?P<D>
458                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
459                  \.?                                       # .
460                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
461                )?''', re.VERBOSE), serialised_re=re.compile('''
462             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
463             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
464             add_granularity=0):
465         ''' set the date to the value in spec
466         '''
467         self.year = self.month = self.week = self.day = self.hour = \
468             self.minute = self.second = 0
469         self.sign = 1
470         m = serialised_re.match(spec)
471         if not m:
472             m = interval_re.match(spec)
473             if not m:
474                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
475                     '[#d] [[[H]H:MM]:SS] [date spec]')
476         else:
477             allowdate = 0
479         # pull out all the info specified
480         info = m.groupdict()
481         if add_granularity:
482             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
484         valid = 0
485         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
486                 'H':'hour', 'M':'minute', 'S':'second'}.items():
487             if info.get(group, None) is not None:
488                 valid = 1
489                 setattr(self, attr, int(info[group]))
491         # make sure it's valid
492         if not valid and not info['D']:
493             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
494                 '[#d] [[[H]H:MM]:SS]')
496         if self.week:
497             self.day = self.day + self.week*7
499         if info['s'] is not None:
500             self.sign = {'+':1, '-':-1}[info['s']]
502         # use a date spec if one is given
503         if allowdate and info['D'] is not None:
504             now = Date('.')
505             date = Date(info['D'])
506             # if no time part was specified, nuke it in the "now" date
507             if not date.hour or date.minute or date.second:
508                 now.hour = now.minute = now.second = 0
509             if date != now:
510                 y = now - (date + self)
511                 self.__init__(y.get_tuple())
513     def __cmp__(self, other):
514         """Compare this interval to another interval."""
515         if other is None:
516             # we are always larger than None
517             return 1
518         for attr in 'sign year month day hour minute second'.split():
519             r = cmp(getattr(self, attr), getattr(other, attr))
520             if r:
521                 return r
522         return 0
524     def __str__(self):
525         """Return this interval as a string."""
526         l = []
527         if self.year: l.append('%sy'%self.year)
528         if self.month: l.append('%sm'%self.month)
529         if self.day: l.append('%sd'%self.day)
530         if self.second:
531             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
532         elif self.hour or self.minute:
533             l.append('%d:%02d'%(self.hour, self.minute))
534         if l:
535             l.insert(0, {1:'+', -1:'-'}[self.sign])
536         return ' '.join(l)
538     def __add__(self, other):
539         if isinstance(other, Date):
540             # the other is a Date - produce a Date
541             return Date(other.addInterval(self))
542         elif isinstance(other, Interval):
543             # add the other Interval to this one
544             a = self.get_tuple()
545             as = a[0]
546             b = other.get_tuple()
547             bs = b[0]
548             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
549             i.insert(0, 1)
550             i = fixTimeOverflow(i)
551             return Interval(i)
552         # nope, no idea what to do with this other...
553         raise TypeError, "Can't add %r"%other
555     def __sub__(self, other):
556         if isinstance(other, Date):
557             # the other is a Date - produce a Date
558             interval = Interval(self.get_tuple())
559             interval.sign *= -1
560             return Date(other.addInterval(interval))
561         elif isinstance(other, Interval):
562             # add the other Interval to this one
563             a = self.get_tuple()
564             as = a[0]
565             b = other.get_tuple()
566             bs = b[0]
567             i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
568             i.insert(0, 1)
569             i = fixTimeOverflow(i)
570             return Interval(i)
571         # nope, no idea what to do with this other...
572         raise TypeError, "Can't add %r"%other
574     def __div__(self, other):
575         """ Divide this interval by an int value.
577             Can't divide years and months sensibly in the _same_
578             calculation as days/time, so raise an error in that situation.
579         """
580         try:
581             other = float(other)
582         except TypeError:
583             raise ValueError, "Can only divide Intervals by numbers"
585         y, m, d, H, M, S = (self.year, self.month, self.day,
586             self.hour, self.minute, self.second)
587         if y or m:
588             if d or H or M or S:
589                 raise ValueError, "Can't divide Interval with date and time"
590             months = self.year*12 + self.month
591             months *= self.sign
593             months = int(months/other)
595             sign = months<0 and -1 or 1
596             m = months%12
597             y = months / 12
598             return Interval((sign, y, m, 0, 0, 0, 0))
600         else:
601             # handle a day/time division
602             seconds = S + M*60 + H*60*60 + d*60*60*24
603             seconds *= self.sign
605             seconds = int(seconds/other)
607             sign = seconds<0 and -1 or 1
608             seconds *= sign
609             S = seconds%60
610             seconds /= 60
611             M = seconds%60
612             seconds /= 60
613             H = seconds%24
614             d = seconds / 24
615             return Interval((sign, 0, 0, d, H, M, S))
617     def __repr__(self):
618         return '<Interval %s>'%self.__str__()
620     def pretty(self):
621         ''' print up the date date using one of these nice formats..
622         '''
623         if self.year:
624             if self.year == 1:
625                 s = _('1 year')
626             else:
627                 s = _('%(number)s years')%{'number': self.year}
628         elif self.month or self.day > 13:
629             days = (self.month * 30) + self.day
630             if days > 28:
631                 if int(days/30) > 1:
632                     s = _('%(number)s months')%{'number': int(days/30)}
633                 else:
634                     s = _('1 month')
635             else:
636                 s = _('%(number)s weeks')%{'number': int(days/7)}
637         elif self.day > 7:
638             s = _('1 week')
639         elif self.day > 1:
640             s = _('%(number)s days')%{'number': self.day}
641         elif self.day == 1 or self.hour > 12:
642             if self.sign > 0:
643                 return _('tomorrow')
644             else:
645                 return _('yesterday')
646         elif self.hour > 1:
647             s = _('%(number)s hours')%{'number': self.hour}
648         elif self.hour == 1:
649             if self.minute < 15:
650                 s = _('an hour')
651             elif self.minute/15 == 2:
652                 s = _('1 1/2 hours')
653             else:
654                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
655         elif self.minute < 1:
656             if self.sign > 0:
657                 return _('in a moment')
658             else:
659                 return _('just now')
660         elif self.minute == 1:
661             s = _('1 minute')
662         elif self.minute < 15:
663             s = _('%(number)s minutes')%{'number': self.minute}
664         elif int(self.minute/15) == 2:
665             s = _('1/2 an hour')
666         else:
667             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
668         if self.sign < 0: 
669             s = s + _(' ago')
670         else:
671             s = _('in ') + s
672         return s
674     def get_tuple(self):
675         return (self.sign, self.year, self.month, self.day, self.hour,
676             self.minute, self.second)
678     def serialise(self):
679         sign = self.sign > 0 and '+' or '-'
680         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
681             self.day, self.hour, self.minute, self.second)
683 def fixTimeOverflow(time):
684     """ Handle the overflow in the time portion (H, M, S) of "time":
685             (sign, y,m,d,H,M,S)
687         Overflow and underflow will at most affect the _days_ portion of
688         the date. We do not overflow days to months as we don't know _how_
689         to, generally.
690     """
691     # XXX we could conceivably use this function for handling regular dates
692     # XXX too - we just need to interrogate the month/year for the day
693     # XXX overflow...
695     sign, y, m, d, H, M, S = time
696     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
697     if seconds:
698         sign = seconds<0 and -1 or 1
699         seconds *= sign
700         S = seconds%60
701         seconds /= 60
702         M = seconds%60
703         seconds /= 60
704         H = seconds%24
705         d = seconds / 24
706     else:
707         months = y*12 + m
708         sign = months<0 and -1 or 1
709         months *= sign
710         m = months%12
711         y = months/12
713     return (sign, y, m, d, H, M, S)
715 class Range:
716     """Represents range between two values
717     Ranges can be created using one of theese two alternative syntaxes:
718         
719     1. Native english syntax::
721             [[From] <value>][ To <value>]
723        Keywords "From" and "To" are case insensitive. Keyword "From" is
724        optional.
726     2. "Geek" syntax::
728           [<value>][; <value>]
730     Either first or second <value> can be omitted in both syntaxes.
732     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003)::
734         >>> Range("from 2-12 to 4-2")
735         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
736         
737         >>> Range("18:00 TO +2m")
738         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
739         
740         >>> Range("12:00")
741         <Range from 2003-03-08.12:00:00 to None>
742         
743         >>> Range("tO +3d")
744         <Range from None to 2003-03-11.20:07:48>
745         
746         >>> Range("2002-11-10; 2002-12-12")
747         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
748         
749         >>> Range("; 20:00 +1d")
750         <Range from None to 2003-03-09.20:00:00>
752     """
753     def __init__(self, spec, Type, allow_granularity=1, **params):
754         """Initializes Range of type <Type> from given <spec> string.
755         
756         Sets two properties - from_value and to_value. None assigned to any of
757         this properties means "infinitum" (-infinitum to from_value and
758         +infinitum to to_value)
760         The Type parameter here should be class itself (e.g. Date), not a
761         class instance.
762         
763         """
764         self.range_type = Type
765         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
766         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
767         # Check which syntax to use
768         if  spec.find(';') == -1:
769             # Native english
770             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
771         else:
772             # Geek
773             mch_range = re.search(re_geek_range, spec.strip())
774         if mch_range:
775             self.from_value, self.to_value = mch_range.groups()
776             if self.from_value:
777                 self.from_value = Type(self.from_value.strip(), **params)
778             if self.to_value:
779                 self.to_value = Type(self.to_value.strip(), **params)
780         else:
781             if allow_granularity:
782                 self.from_value = Type(spec, **params)
783                 self.to_value = Type(spec, add_granularity=1, **params)
784             else:
785                 raise ValueError, "Invalid range"
787     def __str__(self):
788         return "from %s to %s" % (self.from_value, self.to_value)
790     def __repr__(self):
791         return "<Range %s>" % self.__str__()
792  
793 def test_range():
794     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
795         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
796     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
797     for rspec in rspecs:
798         print '>>> Range("%s")' % rspec
799         print `Range(rspec, Date)`
800         print
801     for rspec in rispecs:
802         print '>>> Range("%s")' % rspec
803         print `Range(rspec, Interval)`
804         print
806 def test():
807     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
808     for interval in intervals:
809         print '>>> Interval("%s")'%interval
810         print `Interval(interval)`
812     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
813         "08-13.22:13", "14:25", '2002-12')
814     for date in dates:
815         print '>>> Date("%s")'%date
816         print `Date(date)`
818     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
819     for date, interval in sums:
820         print '>>> Date("%s") + Interval("%s")'%(date, interval)
821         print `Date(date) + Interval(interval)`
823 if __name__ == '__main__':
824     test()
826 # vim: set filetype=python ts=4 sw=4 et si