Code

Refactored CGI file serving so that FileClass contents are a) read more
[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.59 2003-12-05 03:28:38 richard Exp $
20 __doc__ = """
21 Date, time and time interval handling.
22 """
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.
55       "2000-04-17" means <Date 2000-04-17.00:00:00>
56       "01-25" means <Date yyyy-01-25.00:00:00>
57       "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
58       "08-13.22:13" means <Date yyyy-08-14.03:13:00>
59       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
60       "14:25" means <Date yyyy-mm-dd.19:25:00>
61       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
62       "2003" means <Date 2003-01-01.00:00:00>
63       "2003-06" means <Date 2003-06-01.00:00:00>
64       "." means "right now"
66     The Date class should understand simple date expressions of the form
67     stamp + interval and stamp - interval. When adding or subtracting
68     intervals involving months or years, the components are handled
69     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
70     first add one month to get 2000-07-25, then add 10 days to get
71     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
72     or 41 days).
74     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.
93     '''
94     
95     def __init__(self, spec='.', offset=0, add_granularity=0):
96         """Construct a date given a specification and a time zone offset.
98           'spec' is a full date or a partial form, with an optional
99                  added or subtracted interval. Or a date 9-tuple.
100         'offset' is the local time zone offset from GMT in hours.
101         """
102         if type(spec) == type(''):
103             self.set(spec, offset=offset, add_granularity=add_granularity)
104         else:
105             y,m,d,H,M,S,x,x,x = spec
106             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
107             self.year, self.month, self.day, self.hour, self.minute, \
108                 self.second, x, x, x = time.gmtime(ts)
110     usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS][offset]'
111     def set(self, spec, offset=0, date_re=re.compile(r'''
112             ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
113             |(?P<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
114             (?P<n>\.)?                                     # .
115             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?  # hh:mm:ss
116             (?P<o>.+)?                                     # offset
117             ''', re.VERBOSE), serialised_re=re.compile(r'''
118             (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
119             ''', re.VERBOSE), add_granularity=0):
120         ''' set the date to the value in spec
121         '''
123         m = serialised_re.match(spec)
124         if m is not None:
125             # we're serialised - easy!
126             self.year, self.month, self.day, self.hour, self.minute, \
127                 self.second = map(int, m.groups()[:6])
128             return
130         # not serialised data, try usual format
131         m = date_re.match(spec)
132         if m is None:
133             raise ValueError, _('Not a date spec: %s' % self.usagespec)
135         info = m.groupdict()
137         if add_granularity:
138             _add_granularity(info, 'SMHdmyab')
140         # get the current date as our default
141         y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
143         if info['y'] is not None or info['a'] is not None:
144             if info['y'] is not None:
145                 y = int(info['y'])
146                 m,d = (1,1)
147                 if info['m'] is not None:
148                     m = int(info['m'])
149                     if info['d'] is not None:
150                         d = int(info['d'])
151             if info['a'] is not None:
152                 m = int(info['a'])
153                 d = int(info['b'])
154             H = -offset
155             M = S = 0
157         # override hour, minute, second parts
158         if info['H'] is not None and info['M'] is not None:
159             H = int(info['H']) - offset
160             M = int(info['M'])
161             S = 0
162             if info['S'] is not None: S = int(info['S'])
164         if add_granularity:
165             S = S - 1
166         
167         # now handle the adjustment of hour
168         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
169         self.year, self.month, self.day, self.hour, self.minute, \
170             self.second, x, x, x = time.gmtime(ts)
172         if info.get('o', None):
173             try:
174                 self.applyInterval(Interval(info['o'], allowdate=0))
175             except ValueError:
176                 raise ValueError, _('Not a date spec: %s' % self.usagespec)
178     def addInterval(self, interval):
179         ''' Add the interval to this date, returning the date tuple
180         '''
181         # do the basic calc
182         sign = interval.sign
183         year = self.year + sign * interval.year
184         month = self.month + sign * interval.month
185         day = self.day + sign * interval.day
186         hour = self.hour + sign * interval.hour
187         minute = self.minute + sign * interval.minute
188         second = self.second + sign * interval.second
190         # now cope with under- and over-flow
191         # first do the time
192         while (second < 0 or second > 59 or minute < 0 or minute > 59 or
193                 hour < 0 or hour > 23):
194             if second < 0: minute -= 1; second += 60
195             elif second > 59: minute += 1; second -= 60
196             if minute < 0: hour -= 1; minute += 60
197             elif minute > 59: hour += 1; minute -= 60
198             if hour < 0: day -= 1; hour += 24
199             elif hour > 23: day += 1; hour -= 24
201         # fix up the month so we're within range
202         while month < 1 or month > 12:
203             if month < 1: year -= 1; month += 12
204             if month > 12: year += 1; month -= 12
206         # now do the days, now that we know what month we're in
207         def get_mdays(year, month):
208             if month == 2 and calendar.isleap(year): return 29
209             else: return calendar.mdays[month]
211         while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
212             # now to day under/over
213             if day < 1: 
214                 # When going backwards, decrement month, then increment days
215                 month -= 1
216                 day += get_mdays(year,month)
217             elif day > get_mdays(year,month): 
218                 # When going forwards, decrement days, then increment month
219                 day -= get_mdays(year,month)
220                 month += 1
222             # possibly fix up the month so we're within range
223             while month < 1 or month > 12:
224                 if month < 1: year -= 1; month += 12 ; day += 31
225                 if month > 12: year += 1; month -= 12
227         return (year, month, day, hour, minute, second, 0, 0, 0)
229     def differenceDate(self, other):
230         "Return the difference between this date and another date"
232     def applyInterval(self, interval):
233         ''' Apply the interval to this date
234         '''
235         self.year, self.month, self.day, self.hour, self.minute, \
236             self.second, x, x, x = self.addInterval(interval)
238     def __add__(self, interval):
239         """Add an interval to this date to produce another date.
240         """
241         return Date(self.addInterval(interval))
243     # deviates from spec to allow subtraction of dates as well
244     def __sub__(self, other):
245         """ Subtract:
246              1. an interval from this date to produce another date.
247              2. a date from this date to produce an interval.
248         """
249         if isinstance(other, Interval):
250             other = Interval(other.get_tuple())
251             other.sign *= -1
252             return self.__add__(other)
254         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
256         return self.dateDelta(other)
258     def dateDelta(self, other):
259         """ Produce an Interval of the difference between this date
260             and another date. Only returns days:hours:minutes:seconds.
261         """
262         # Returning intervals larger than a day is almost
263         # impossible - months, years, weeks, are all so imprecise.
264         a = calendar.timegm((self.year, self.month, self.day, self.hour,
265             self.minute, self.second, 0, 0, 0))
266         b = calendar.timegm((other.year, other.month, other.day,
267             other.hour, other.minute, other.second, 0, 0, 0))
268         diff = a - b
269         if diff > 0:
270             sign = 1
271         else:
272             sign = -1
273             diff = -diff
274         S = diff%60
275         M = (diff/60)%60
276         H = (diff/(60*60))%24
277         d = diff/(24*60*60)
278         return Interval((0, 0, d, H, M, S), sign=sign)
280     def __cmp__(self, other):
281         """Compare this date to another date."""
282         if other is None:
283             return 1
284         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
285             if not hasattr(other, attr):
286                 return 1
287             r = cmp(getattr(self, attr), getattr(other, attr))
288             if r: return r
289         return 0
291     def __str__(self):
292         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
293         return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
294             self.hour, self.minute, self.second)
296     def pretty(self, format='%d %B %Y'):
297         ''' print up the date date using a pretty format...
299             Note that if the day is zero, and the day appears first in the
300             format, then the day number will be removed from output.
301         '''
302         str = time.strftime(format, (self.year, self.month, self.day,
303             self.hour, self.minute, self.second, 0, 0, 0))
304         # handle zero day by removing it
305         if format.startswith('%d') and str[0] == '0':
306             return ' ' + str[1:]
307         return str
309     def __repr__(self):
310         return '<Date %s>'%self.__str__()
312     def local(self, offset):
313         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
314         """
315         return Date((self.year, self.month, self.day, self.hour + offset,
316             self.minute, self.second, 0, 0, 0))
318     def get_tuple(self):
319         return (self.year, self.month, self.day, self.hour, self.minute,
320             self.second, 0, 0, 0)
322     def serialise(self):
323         return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
324             self.day, self.hour, self.minute, self.second)
326     def timestamp(self):
327         ''' return a UNIX timestamp for this date '''
328         return calendar.timegm((self.year, self.month, self.day, self.hour,
329             self.minute, self.second, 0, 0, 0))
331 class Interval:
332     '''
333     Date intervals are specified using the suffixes "y", "m", and "d". The
334     suffix "w" (for "week") means 7 days. Time intervals are specified in
335     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
336     may not).
338       "3y" means three years
339       "2y 1m" means two years and one month
340       "1m 25d" means one month and 25 days
341       "2w 3d" means two weeks and three days
342       "1d 2:50" means one day, two hours, and 50 minutes
343       "14:00" means 14 hours
344       "0:04:33" means four minutes and 33 seconds
346     Example usage:
347         >>> Interval("  3w  1  d  2:00")
348         <Interval + 22d 2:00>
349         >>> Date(". + 2d") + Interval("- 3w")
350         <Date 2000-06-07.00:34:02>
351         >>> Interval('1:59:59') + Interval('00:00:01')
352         <Interval + 2:00>
353         >>> Interval('2:00') + Interval('- 00:00:01')
354         <Interval + 1:59:59>
355         >>> Interval('1y')/2
356         <Interval + 6m>
357         >>> Interval('1:00')/2
358         <Interval + 0:30>
359         >>> Interval('2003-03-18')
360         <Interval + [number of days between now and 2003-03-18]>
361         >>> Interval('-4d 2003-03-18')
362         <Interval + [number of days between now and 2003-03-14]>
364     Interval arithmetic is handled in a couple of special ways, trying
365     to cater for the most common cases. Fundamentally, Intervals which
366     have both date and time parts will result in strange results in
367     arithmetic - because of the impossibility of handling day->month->year
368     over- and under-flows. Intervals may also be divided by some number.
370     Intervals are added to Dates in order of:
371        seconds, minutes, hours, years, months, days
373     Calculations involving months (eg '+2m') have no effect on days - only
374     days (or over/underflow from hours/mins/secs) will do that, and
375     days-per-month and leap years are accounted for. Leap seconds are not.
377     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
378     minute, second) is the serialisation format returned by the serialise()
379     method, and is accepted as an argument on instatiation.
381     TODO: more examples, showing the order of addition operation
382     '''
383     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
384         """Construct an interval given a specification."""
385         if type(spec) == type(''):
386             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
387         else:
388             if len(spec) == 7:
389                 self.sign, self.year, self.month, self.day, self.hour, \
390                     self.minute, self.second = spec
391             else:
392                 # old, buggy spec form
393                 self.sign = sign
394                 self.year, self.month, self.day, self.hour, self.minute, \
395                     self.second = spec
397     def set(self, spec, allowdate=1, interval_re=re.compile('''
398             \s*(?P<s>[-+])?         # + or -
399             \s*((?P<y>\d+\s*)y)?    # year
400             \s*((?P<m>\d+\s*)m)?    # month
401             \s*((?P<w>\d+\s*)w)?    # week
402             \s*((?P<d>\d+\s*)d)?    # day
403             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
404             \s*(?P<D>
405                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
406                  \.?                                       # .
407                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
408                )?''', re.VERBOSE), serialised_re=re.compile('''
409             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
410             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
411             add_granularity=0):
412         ''' set the date to the value in spec
413         '''
414         self.year = self.month = self.week = self.day = self.hour = \
415             self.minute = self.second = 0
416         self.sign = 1
417         m = serialised_re.match(spec)
418         if not m:
419             m = interval_re.match(spec)
420             if not m:
421                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
422                     '[#d] [[[H]H:MM]:SS] [date spec]')
423         else:
424             allowdate = 0
426         # pull out all the info specified
427         info = m.groupdict()
428         if add_granularity:
429             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
431         valid = 0
432         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
433                 'H':'hour', 'M':'minute', 'S':'second'}.items():
434             if info.get(group, None) is not None:
435                 valid = 1
436                 setattr(self, attr, int(info[group]))
438         # make sure it's valid
439         if not valid and not info['D']:
440             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
441                 '[#d] [[[H]H:MM]:SS]')
443         if self.week:
444             self.day = self.day + self.week*7
446         if info['s'] is not None:
447             self.sign = {'+':1, '-':-1}[info['s']]
449         # use a date spec if one is given
450         if allowdate and info['D'] is not None:
451             now = Date('.')
452             date = Date(info['D'])
453             # if no time part was specified, nuke it in the "now" date
454             if not date.hour or date.minute or date.second:
455                 now.hour = now.minute = now.second = 0
456             if date != now:
457                 y = now - (date + self)
458                 self.__init__(y.get_tuple())
460     def __cmp__(self, other):
461         """Compare this interval to another interval."""
462         if other is None:
463             # we are always larger than None
464             return 1
465         for attr in 'sign year month day hour minute second'.split():
466             r = cmp(getattr(self, attr), getattr(other, attr))
467             if r:
468                 return r
469         return 0
471     def __str__(self):
472         """Return this interval as a string."""
473         l = []
474         if self.year: l.append('%sy'%self.year)
475         if self.month: l.append('%sm'%self.month)
476         if self.day: l.append('%sd'%self.day)
477         if self.second:
478             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
479         elif self.hour or self.minute:
480             l.append('%d:%02d'%(self.hour, self.minute))
481         if l:
482             l.insert(0, {1:'+', -1:'-'}[self.sign])
483         return ' '.join(l)
485     def __add__(self, other):
486         if isinstance(other, Date):
487             # the other is a Date - produce a Date
488             return Date(other.addInterval(self))
489         elif isinstance(other, Interval):
490             # add the other Interval to this one
491             a = self.get_tuple()
492             as = a[0]
493             b = other.get_tuple()
494             bs = b[0]
495             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
496             i.insert(0, 1)
497             i = fixTimeOverflow(i)
498             return Interval(i)
499         # nope, no idea what to do with this other...
500         raise TypeError, "Can't add %r"%other
502     def __sub__(self, other):
503         if isinstance(other, Date):
504             # the other is a Date - produce a Date
505             interval = Interval(self.get_tuple())
506             interval.sign *= -1
507             return Date(other.addInterval(interval))
508         elif isinstance(other, Interval):
509             # add the other Interval to this one
510             a = self.get_tuple()
511             as = a[0]
512             b = other.get_tuple()
513             bs = b[0]
514             i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
515             i.insert(0, 1)
516             i = fixTimeOverflow(i)
517             return Interval(i)
518         # nope, no idea what to do with this other...
519         raise TypeError, "Can't add %r"%other
521     def __div__(self, other):
522         """ Divide this interval by an int value.
524             Can't divide years and months sensibly in the _same_
525             calculation as days/time, so raise an error in that situation.
526         """
527         try:
528             other = float(other)
529         except TypeError:
530             raise ValueError, "Can only divide Intervals by numbers"
532         y, m, d, H, M, S = (self.year, self.month, self.day,
533             self.hour, self.minute, self.second)
534         if y or m:
535             if d or H or M or S:
536                 raise ValueError, "Can't divide Interval with date and time"
537             months = self.year*12 + self.month
538             months *= self.sign
540             months = int(months/other)
542             sign = months<0 and -1 or 1
543             m = months%12
544             y = months / 12
545             return Interval((sign, y, m, 0, 0, 0, 0))
547         else:
548             # handle a day/time division
549             seconds = S + M*60 + H*60*60 + d*60*60*24
550             seconds *= self.sign
552             seconds = int(seconds/other)
554             sign = seconds<0 and -1 or 1
555             seconds *= sign
556             S = seconds%60
557             seconds /= 60
558             M = seconds%60
559             seconds /= 60
560             H = seconds%24
561             d = seconds / 24
562             return Interval((sign, 0, 0, d, H, M, S))
564     def __repr__(self):
565         return '<Interval %s>'%self.__str__()
567     def pretty(self):
568         ''' print up the date date using one of these nice formats..
569         '''
570         if self.year:
571             if self.year == 1:
572                 s = _('1 year')
573             else:
574                 s = _('%(number)s years')%{'number': self.year}
575         elif self.month or self.day > 13:
576             days = (self.month * 30) + self.day
577             if days > 28:
578                 if int(days/30) > 1:
579                     s = _('%(number)s months')%{'number': int(days/30)}
580                 else:
581                     s = _('1 month')
582             else:
583                 s = _('%(number)s weeks')%{'number': int(days/7)}
584         elif self.day > 7:
585             s = _('1 week')
586         elif self.day > 1:
587             s = _('%(number)s days')%{'number': self.day}
588         elif self.day == 1 or self.hour > 12:
589             if self.sign > 0:
590                 return _('tomorrow')
591             else:
592                 return _('yesterday')
593         elif self.hour > 1:
594             s = _('%(number)s hours')%{'number': self.hour}
595         elif self.hour == 1:
596             if self.minute < 15:
597                 s = _('an hour')
598             elif self.minute/15 == 2:
599                 s = _('1 1/2 hours')
600             else:
601                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
602         elif self.minute < 1:
603             if self.sign > 0:
604                 return _('in a moment')
605             else:
606                 return _('just now')
607         elif self.minute == 1:
608             s = _('1 minute')
609         elif self.minute < 15:
610             s = _('%(number)s minutes')%{'number': self.minute}
611         elif int(self.minute/15) == 2:
612             s = _('1/2 an hour')
613         else:
614             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
615         if self.sign < 0: 
616             s = s + _(' ago')
617         else:
618             s = _('in ') + s
619         return s
621     def get_tuple(self):
622         return (self.sign, self.year, self.month, self.day, self.hour,
623             self.minute, self.second)
625     def serialise(self):
626         sign = self.sign > 0 and '+' or '-'
627         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
628             self.day, self.hour, self.minute, self.second)
630 def fixTimeOverflow(time):
631     """ Handle the overflow in the time portion (H, M, S) of "time":
632             (sign, y,m,d,H,M,S)
634         Overflow and underflow will at most affect the _days_ portion of
635         the date. We do not overflow days to months as we don't know _how_
636         to, generally.
637     """
638     # XXX we could conceivably use this function for handling regular dates
639     # XXX too - we just need to interrogate the month/year for the day
640     # XXX overflow...
642     sign, y, m, d, H, M, S = time
643     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
644     if seconds:
645         sign = seconds<0 and -1 or 1
646         seconds *= sign
647         S = seconds%60
648         seconds /= 60
649         M = seconds%60
650         seconds /= 60
651         H = seconds%24
652         d = seconds / 24
653     else:
654         months = y*12 + m
655         sign = months<0 and -1 or 1
656         months *= sign
657         m = months%12
658         y = months/12
660     return (sign, y, m, d, H, M, S)
662 class Range:
663     """
664     Represents range between two values
665     Ranges can be created using one of theese two alternative syntaxes:
666         
667         1. Native english syntax: 
668             [[From] <value>][ To <value>]
669            Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
671         2. "Geek" syntax:
672             [<value>][; <value>]
674     Either first or second <value> can be omitted in both syntaxes.
676     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003):
677         >>> Range("from 2-12 to 4-2")
678         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
679         
680         >>> Range("18:00 TO +2m")
681         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
682         
683         >>> Range("12:00")
684         <Range from 2003-03-08.12:00:00 to None>
685         
686         >>> Range("tO +3d")
687         <Range from None to 2003-03-11.20:07:48>
688         
689         >>> Range("2002-11-10; 2002-12-12")
690         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
691         
692         >>> Range("; 20:00 +1d")
693         <Range from None to 2003-03-09.20:00:00>
695     """
696     def __init__(self, spec, Type, allow_granularity=1, **params):
697         """Initializes Range of type <Type> from given <spec> string.
698         
699         Sets two properties - from_value and to_value. None assigned to any of
700         this properties means "infinitum" (-infinitum to from_value and
701         +infinitum to to_value)
703         The Type parameter here should be class itself (e.g. Date), not a
704         class instance.
705         
706         """
707         self.range_type = Type
708         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
709         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
710         # Check which syntax to use
711         if  spec.find(';') == -1:
712             # Native english
713             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
714         else:
715             # Geek
716             mch_range = re.search(re_geek_range, spec.strip())
717         if mch_range:
718             self.from_value, self.to_value = mch_range.groups()
719             if self.from_value:
720                 self.from_value = Type(self.from_value.strip(), **params)
721             if self.to_value:
722                 self.to_value = Type(self.to_value.strip(), **params)
723         else:
724             if allow_granularity:
725                 self.from_value = Type(spec, **params)
726                 self.to_value = Type(spec, add_granularity=1, **params)
727             else:
728                 raise ValueError, "Invalid range"
730     def __str__(self):
731         return "from %s to %s" % (self.from_value, self.to_value)
733     def __repr__(self):
734         return "<Range %s>" % self.__str__()
735  
736 def test_range():
737     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
738         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
739     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
740     for rspec in rspecs:
741         print '>>> Range("%s")' % rspec
742         print `Range(rspec, Date)`
743         print
744     for rspec in rispecs:
745         print '>>> Range("%s")' % rspec
746         print `Range(rspec, Interval)`
747         print
749 def test():
750     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
751     for interval in intervals:
752         print '>>> Interval("%s")'%interval
753         print `Interval(interval)`
755     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
756         "08-13.22:13", "14:25", '2002-12')
757     for date in dates:
758         print '>>> Date("%s")'%date
759         print `Date(date)`
761     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
762     for date, interval in sums:
763         print '>>> Date("%s") + Interval("%s")'%(date, interval)
764         print `Date(date) + Interval(interval)`
766 if __name__ == '__main__':
767     test()
769 # vim: set filetype=python ts=4 sw=4 et si