Code

d4f5f36d8acae0e51fd8d29120e530678631d579
[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.56 2003-11-04 12:35:47 anthonybaxter 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 > 59):
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 > 59: 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]
210             
211         while month < 1 or month > 12 or day < 0 or day > get_mdays(year,month):
212             # now to day under/over
213             if day < 0: 
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 class Interval:
327     '''
328     Date intervals are specified using the suffixes "y", "m", and "d". The
329     suffix "w" (for "week") means 7 days. Time intervals are specified in
330     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
331     may not).
333       "3y" means three years
334       "2y 1m" means two years and one month
335       "1m 25d" means one month and 25 days
336       "2w 3d" means two weeks and three days
337       "1d 2:50" means one day, two hours, and 50 minutes
338       "14:00" means 14 hours
339       "0:04:33" means four minutes and 33 seconds
341     Example usage:
342         >>> Interval("  3w  1  d  2:00")
343         <Interval + 22d 2:00>
344         >>> Date(". + 2d") + Interval("- 3w")
345         <Date 2000-06-07.00:34:02>
346         >>> Interval('1:59:59') + Interval('00:00:01')
347         <Interval + 2:00>
348         >>> Interval('2:00') + Interval('- 00:00:01')
349         <Interval + 1:59:59>
350         >>> Interval('1y')/2
351         <Interval + 6m>
352         >>> Interval('1:00')/2
353         <Interval + 0:30>
354         >>> Interval('2003-03-18')
355         <Interval + [number of days between now and 2003-03-18]>
356         >>> Interval('-4d 2003-03-18')
357         <Interval + [number of days between now and 2003-03-14]>
359     Interval arithmetic is handled in a couple of special ways, trying
360     to cater for the most common cases. Fundamentally, Intervals which
361     have both date and time parts will result in strange results in
362     arithmetic - because of the impossibility of handling day->month->year
363     over- and under-flows. Intervals may also be divided by some number.
365     Intervals are added to Dates in order of:
366        seconds, minutes, hours, years, months, days
368     Calculations involving months (eg '+2m') have no effect on days - only
369     days (or over/underflow from hours/mins/secs) will do that, and
370     days-per-month and leap years are accounted for. Leap seconds are not.
372     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
373     minute, second) is the serialisation format returned by the serialise()
374     method, and is accepted as an argument on instatiation.
376     TODO: more examples, showing the order of addition operation
377     '''
378     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
379         """Construct an interval given a specification."""
380         if type(spec) == type(''):
381             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
382         else:
383             if len(spec) == 7:
384                 self.sign, self.year, self.month, self.day, self.hour, \
385                     self.minute, self.second = spec
386             else:
387                 # old, buggy spec form
388                 self.sign = sign
389                 self.year, self.month, self.day, self.hour, self.minute, \
390                     self.second = spec
392     def set(self, spec, allowdate=1, interval_re=re.compile('''
393             \s*(?P<s>[-+])?         # + or -
394             \s*((?P<y>\d+\s*)y)?    # year
395             \s*((?P<m>\d+\s*)m)?    # month
396             \s*((?P<w>\d+\s*)w)?    # week
397             \s*((?P<d>\d+\s*)d)?    # day
398             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
399             \s*(?P<D>
400                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
401                  \.?                                       # .
402                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
403                )?''', re.VERBOSE), serialised_re=re.compile('''
404             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
405             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
406             add_granularity=0):
407         ''' set the date to the value in spec
408         '''
409         self.year = self.month = self.week = self.day = self.hour = \
410             self.minute = self.second = 0
411         self.sign = 1
412         m = serialised_re.match(spec)
413         if not m:
414             m = interval_re.match(spec)
415             if not m:
416                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
417                     '[#d] [[[H]H:MM]:SS] [date spec]')
418         else:
419             allowdate = 0
421         # pull out all the info specified
422         info = m.groupdict()
423         if add_granularity:
424             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
426         valid = 0
427         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
428                 'H':'hour', 'M':'minute', 'S':'second'}.items():
429             if info.get(group, None) is not None:
430                 valid = 1
431                 setattr(self, attr, int(info[group]))
433         # make sure it's valid
434         if not valid and not info['D']:
435             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
436                 '[#d] [[[H]H:MM]:SS]')
438         if self.week:
439             self.day = self.day + self.week*7
441         if info['s'] is not None:
442             self.sign = {'+':1, '-':-1}[info['s']]
444         # use a date spec if one is given
445         if allowdate and info['D'] is not None:
446             now = Date('.')
447             date = Date(info['D'])
448             # if no time part was specified, nuke it in the "now" date
449             if not date.hour or date.minute or date.second:
450                 now.hour = now.minute = now.second = 0
451             if date != now:
452                 y = now - (date + self)
453                 self.__init__(y.get_tuple())
455     def __cmp__(self, other):
456         """Compare this interval to another interval."""
457         if other is None:
458             # we are always larger than None
459             return 1
460         for attr in 'sign year month day hour minute second'.split():
461             r = cmp(getattr(self, attr), getattr(other, attr))
462             if r:
463                 return r
464         return 0
466     def __str__(self):
467         """Return this interval as a string."""
468         l = []
469         if self.year: l.append('%sy'%self.year)
470         if self.month: l.append('%sm'%self.month)
471         if self.day: l.append('%sd'%self.day)
472         if self.second:
473             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
474         elif self.hour or self.minute:
475             l.append('%d:%02d'%(self.hour, self.minute))
476         if l:
477             l.insert(0, {1:'+', -1:'-'}[self.sign])
478         return ' '.join(l)
480     def __add__(self, other):
481         if isinstance(other, Date):
482             # the other is a Date - produce a Date
483             return Date(other.addInterval(self))
484         elif isinstance(other, Interval):
485             # add the other Interval to this one
486             a = self.get_tuple()
487             as = a[0]
488             b = other.get_tuple()
489             bs = b[0]
490             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
491             i.insert(0, 1)
492             i = fixTimeOverflow(i)
493             return Interval(i)
494         # nope, no idea what to do with this other...
495         raise TypeError, "Can't add %r"%other
497     def __sub__(self, other):
498         if isinstance(other, Date):
499             # the other is a Date - produce a Date
500             interval = Interval(self.get_tuple())
501             interval.sign *= -1
502             return Date(other.addInterval(interval))
503         elif isinstance(other, Interval):
504             # add the other Interval to this one
505             a = self.get_tuple()
506             as = a[0]
507             b = other.get_tuple()
508             bs = b[0]
509             i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
510             i.insert(0, 1)
511             i = fixTimeOverflow(i)
512             return Interval(i)
513         # nope, no idea what to do with this other...
514         raise TypeError, "Can't add %r"%other
516     def __div__(self, other):
517         ''' Divide this interval by an int value.
519             Can't divide years and months sensibly in the _same_
520             calculation as days/time, so raise an error in that situation.
521         '''
522         try:
523             other = float(other)
524         except TypeError:
525             raise ValueError, "Can only divide Intervals by numbers"
527         y, m, d, H, M, S = (self.year, self.month, self.day,
528             self.hour, self.minute, self.second)
529         if y or m:
530             if d or H or M or S:
531                 raise ValueError, "Can't divide Interval with date and time"
532             months = self.year*12 + self.month
533             months *= self.sign
535             months = int(months/other)
537             sign = months<0 and -1 or 1
538             m = months%12
539             y = months / 12
540             return Interval((sign, y, m, 0, 0, 0, 0))
542         else:
543             # handle a day/time division
544             seconds = S + M*60 + H*60*60 + d*60*60*24
545             seconds *= self.sign
547             seconds = int(seconds/other)
549             sign = seconds<0 and -1 or 1
550             seconds *= sign
551             S = seconds%60
552             seconds /= 60
553             M = seconds%60
554             seconds /= 60
555             H = seconds%24
556             d = seconds / 24
557             return Interval((sign, 0, 0, d, H, M, S))
559     def __repr__(self):
560         return '<Interval %s>'%self.__str__()
562     def pretty(self):
563         ''' print up the date date using one of these nice formats..
564         '''
565         if self.year:
566             if self.year == 1:
567                 return _('1 year')
568             else:
569                 return _('%(number)s years')%{'number': self.year}
570         elif self.month or self.day > 13:
571             days = (self.month * 30) + self.day
572             if days > 28:
573                 if int(days/30) > 1:
574                     s = _('%(number)s months')%{'number': int(days/30)}
575                 else:
576                     s = _('1 month')
577             else:
578                 s = _('%(number)s weeks')%{'number': int(days/7)}
579         elif self.day > 7:
580             s = _('1 week')
581         elif self.day > 1:
582             s = _('%(number)s days')%{'number': self.day}
583         elif self.day == 1 or self.hour > 12:
584             if self.sign > 0:
585                 return _('tomorrow')
586             else:
587                 return _('yesterday')
588         elif self.hour > 1:
589             s = _('%(number)s hours')%{'number': self.hour}
590         elif self.hour == 1:
591             if self.minute < 15:
592                 s = _('an hour')
593             elif self.minute/15 == 2:
594                 s = _('1 1/2 hours')
595             else:
596                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
597         elif self.minute < 1:
598             if self.sign > 0:
599                 return _('in a moment')
600             else:
601                 return _('just now')
602         elif self.minute == 1:
603             s = _('1 minute')
604         elif self.minute < 15:
605             s = _('%(number)s minutes')%{'number': self.minute}
606         elif int(self.minute/15) == 2:
607             s = _('1/2 an hour')
608         else:
609             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
610         if self.sign < 0: 
611             s = s + _(' ago')
612         else:
613             s = _('in ') + s
614         return s
616     def get_tuple(self):
617         return (self.sign, self.year, self.month, self.day, self.hour,
618             self.minute, self.second)
620     def serialise(self):
621         sign = self.sign > 0 and '+' or '-'
622         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
623             self.day, self.hour, self.minute, self.second)
625 def fixTimeOverflow(time):
626     ''' Handle the overflow in the time portion (H, M, S) of "time":
627             (sign, y,m,d,H,M,S)
629         Overflow and underflow will at most affect the _days_ portion of
630         the date. We do not overflow days to months as we don't know _how_
631         to, generally.
632     '''
633     # XXX we could conceivably use this function for handling regular dates
634     # XXX too - we just need to interrogate the month/year for the day
635     # XXX overflow...
637     sign, y, m, d, H, M, S = time
638     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
639     if seconds:
640         sign = seconds<0 and -1 or 1
641         seconds *= sign
642         S = seconds%60
643         seconds /= 60
644         M = seconds%60
645         seconds /= 60
646         H = seconds%24
647         d = seconds / 24
648     else:
649         months = y*12 + m
650         sign = months<0 and -1 or 1
651         months *= sign
652         m = months%12
653         y = months/12
655     return (sign, y, m, d, H, M, S)
657 class Range:
658     """
659     Represents range between two values
660     Ranges can be created using one of theese two alternative syntaxes:
661         
662         1. Native english syntax: 
663             [[From] <value>][ To <value>]
664            Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
666         2. "Geek" syntax:
667             [<value>][; <value>]
669     Either first or second <value> can be omitted in both syntaxes.
671     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003):
672         >>> Range("from 2-12 to 4-2")
673         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
674         
675         >>> Range("18:00 TO +2m")
676         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
677         
678         >>> Range("12:00")
679         <Range from 2003-03-08.12:00:00 to None>
680         
681         >>> Range("tO +3d")
682         <Range from None to 2003-03-11.20:07:48>
683         
684         >>> Range("2002-11-10; 2002-12-12")
685         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
686         
687         >>> Range("; 20:00 +1d")
688         <Range from None to 2003-03-09.20:00:00>
690     """
691     def __init__(self, spec, Type, allow_granularity=1, **params):
692         """Initializes Range of type <Type> from given <spec> string.
693         
694         Sets two properties - from_value and to_value. None assigned to any of
695         this properties means "infinitum" (-infinitum to from_value and
696         +infinitum to to_value)
698         The Type parameter here should be class itself (e.g. Date), not a
699         class instance.
700         
701         """
702         self.range_type = Type
703         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
704         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
705         # Check which syntax to use
706         if  spec.find(';') == -1:
707             # Native english
708             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
709         else:
710             # Geek
711             mch_range = re.search(re_geek_range, spec.strip())
712         if mch_range:
713             self.from_value, self.to_value = mch_range.groups()
714             if self.from_value:
715                 self.from_value = Type(self.from_value.strip(), **params)
716             if self.to_value:
717                 self.to_value = Type(self.to_value.strip(), **params)
718         else:
719             if allow_granularity:
720                 self.from_value = Type(spec, **params)
721                 self.to_value = Type(spec, add_granularity=1, **params)
722             else:
723                 raise ValueError, "Invalid range"
725     def __str__(self):
726         return "from %s to %s" % (self.from_value, self.to_value)
728     def __repr__(self):
729         return "<Range %s>" % self.__str__()
730  
731 def test_range():
732     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
733         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
734     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
735     for rspec in rspecs:
736         print '>>> Range("%s")' % rspec
737         print `Range(rspec, Date)`
738         print
739     for rspec in rispecs:
740         print '>>> Range("%s")' % rspec
741         print `Range(rspec, Interval)`
742         print
744 def test():
745     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
746     for interval in intervals:
747         print '>>> Interval("%s")'%interval
748         print `Interval(interval)`
750     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
751         "08-13.22:13", "14:25", '2002-12')
752     for date in dates:
753         print '>>> Date("%s")'%date
754         print `Date(date)`
756     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
757     for date, interval in sums:
758         print '>>> Date("%s") + Interval("%s")'%(date, interval)
759         print `Date(date) + Interval(interval)`
761 if __name__ == '__main__':
762     test()
764 # vim: set filetype=python ts=4 sw=4 et si