Code

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