Code

granularity based ranges
[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.52 2003-04-21 14:29:39 kedder 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       "." means "right now"
64     The Date class should understand simple date expressions of the form
65     stamp + interval and stamp - interval. When adding or subtracting
66     intervals involving months or years, the components are handled
67     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
68     first add one month to get 2000-07-25, then add 10 days to get
69     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
70     or 41 days).
72     Example usage:
73         >>> Date(".")
74         <Date 2000-06-26.00:34:02>
75         >>> _.local(-5)
76         "2000-06-25.19:34:02"
77         >>> Date(". + 2d")
78         <Date 2000-06-28.00:34:02>
79         >>> Date("1997-04-17", -5)
80         <Date 1997-04-17.00:00:00>
81         >>> Date("01-25", -5)
82         <Date 2000-01-25.00:00:00>
83         >>> Date("08-13.22:13", -5)
84         <Date 2000-08-14.03:13:00>
85         >>> Date("14:25", -5)
86         <Date 2000-06-25.19:25:00>
88     The date format 'yyyymmddHHMMSS' (year, month, day, hour,
89     minute, second) is the serialisation format returned by the serialise()
90     method, and is accepted as an argument on instatiation.
91     '''
92     def __init__(self, spec='.', offset=0, add_granularity=0):
93         """Construct a date given a specification and a time zone offset.
95           'spec' is a full date or a partial form, with an optional
96                  added or subtracted interval. Or a date 9-tuple.
97         'offset' is the local time zone offset from GMT in hours.
98         """
99         if type(spec) == type(''):
100             self.set(spec, offset=offset, add_granularity=add_granularity)
101         else:
102             y,m,d,H,M,S,x,x,x = spec
103             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
104             self.year, self.month, self.day, self.hour, self.minute, \
105                 self.second, x, x, x = time.gmtime(ts)
107     def set(self, spec, offset=0, date_re=re.compile(r'''
108             (((?P<y>\d\d\d\d)[/-])?(?P<m>\d\d?)?[/-](?P<d>\d\d?))? # [yyyy-]mm-dd
109             (?P<n>\.)?                                     # .
110             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?  # hh:mm:ss
111             (?P<o>.+)?                                     # offset
112             ''', re.VERBOSE), serialised_re=re.compile(r'''
113             (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
114             ''', re.VERBOSE), add_granularity=0):
115         ''' set the date to the value in spec
116         '''
118         m = serialised_re.match(spec)
119         if m is not None:
120             # we're serialised - easy!
121             self.year, self.month, self.day, self.hour, self.minute, \
122                 self.second = map(int, m.groups()[:6])
123             return
125         # not serialised data, try usual format
126         m = date_re.match(spec)
127         if m is None:
128             raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
129                 '[[h]h:mm[:ss]][offset]')
131         info = m.groupdict()
133         if add_granularity:
134             _add_granularity(info, 'SMHdmy')
136         # get the current date as our default
137         y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
139         # override year, month, day parts
140         if info['m'] is not None and info['d'] is not None:
141             m = int(info['m'])
142             d = int(info['d'])
143             if info['y'] is not None:
144                 y = int(info['y'])
145             # time defaults to 00:00:00 GMT - offset (local midnight)
146             H = -offset
147             M = S = 0
149         # override hour, minute, second parts
150         if info['H'] is not None and info['M'] is not None:
151             H = int(info['H']) - offset
152             M = int(info['M'])
153             S = 0
154             if info['S'] is not None: S = int(info['S'])
156         if add_granularity:
157             S = S - 1
158         
159         # now handle the adjustment of hour
160         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
161         self.year, self.month, self.day, self.hour, self.minute, \
162             self.second, x, x, x = time.gmtime(ts)
164         if info.get('o', None):
165             try:
166                 self.applyInterval(Interval(info['o'], allowdate=0))
167             except ValueError:
168                 raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
169                     '[[h]h:mm[:ss]][offset]')
171     def addInterval(self, interval):
172         ''' Add the interval to this date, returning the date tuple
173         '''
174         # do the basic calc
175         sign = interval.sign
176         year = self.year + sign * interval.year
177         month = self.month + sign * interval.month
178         day = self.day + sign * interval.day
179         hour = self.hour + sign * interval.hour
180         minute = self.minute + sign * interval.minute
181         second = self.second + sign * interval.second
183         # now cope with under- and over-flow
184         # first do the time
185         while (second < 0 or second > 59 or minute < 0 or minute > 59 or
186                 hour < 0 or hour > 59):
187             if second < 0: minute -= 1; second += 60
188             elif second > 59: minute += 1; second -= 60
189             if minute < 0: hour -= 1; minute += 60
190             elif minute > 59: hour += 1; minute -= 60
191             if hour < 0: day -= 1; hour += 24
192             elif hour > 59: day += 1; hour -= 24
194         # fix up the month so we're within range
195         while month < 1 or month > 12:
196             if month < 1: year -= 1; month += 12
197             if month > 12: year += 1; month -= 12
199         # now do the days, now that we know what month we're in
200         mdays = calendar.mdays
201         if month == 2 and calendar.isleap(year): month_days = 29
202         else: month_days = mdays[month]
203         while month < 1 or month > 12 or day < 0 or day > month_days:
204             # now to day under/over
205             if day < 0: month -= 1; day += month_days
206             elif day > month_days: month += 1; day -= month_days
208             # possibly fix up the month so we're within range
209             while month < 1 or month > 12:
210                 if month < 1: year -= 1; month += 12
211                 if month > 12: year += 1; month -= 12
213             # re-figure the number of days for this month
214             if month == 2 and calendar.isleap(year): month_days = 29
215             else: month_days = mdays[month]
216         return (year, month, day, hour, minute, second, 0, 0, 0)
218     def applyInterval(self, interval):
219         ''' Apply the interval to this date
220         '''
221         self.year, self.month, self.day, self.hour, self.minute, \
222             self.second, x, x, x = self.addInterval(interval)
224     def __add__(self, interval):
225         """Add an interval to this date to produce another date.
226         """
227         return Date(self.addInterval(interval))
229     # deviates from spec to allow subtraction of dates as well
230     def __sub__(self, other):
231         """ Subtract:
232              1. an interval from this date to produce another date.
233              2. a date from this date to produce an interval.
234         """
235         if isinstance(other, Interval):
236             other = Interval(other.get_tuple())
237             other.sign *= -1
238             return self.__add__(other)
240         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
242         # TODO this code will fall over laughing if the dates cross
243         # leap years, phases of the moon, ....
244         a = calendar.timegm((self.year, self.month, self.day, self.hour,
245             self.minute, self.second, 0, 0, 0))
246         b = calendar.timegm((other.year, other.month, other.day,
247             other.hour, other.minute, other.second, 0, 0, 0))
248         diff = a - b
249         if diff < 0:
250             sign = 1
251             diff = -diff
252         else:
253             sign = -1
254         S = diff%60
255         M = (diff/60)%60
256         H = (diff/(60*60))%60
257         if H>1: S = 0
258         d = (diff/(24*60*60))%30
259         if d>1: H = S = M = 0
260         m = (diff/(30*24*60*60))%12
261         if m>1: H = S = M = 0
262         y = (diff/(365*24*60*60))
263         if y>1: d = H = S = M = 0
264         return Interval((y, m, d, H, M, S), sign=sign)
266     def __cmp__(self, other):
267         """Compare this date to another date."""
268         if other is None:
269             return 1
270         for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
271             if not hasattr(other, attr):
272                 return 1
273             r = cmp(getattr(self, attr), getattr(other, attr))
274             if r: return r
275         return 0
277     def __str__(self):
278         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
279         return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
280             self.hour, self.minute, self.second)
282     def pretty(self, format='%d %B %Y'):
283         ''' print up the date date using a pretty format...
285             Note that if the day is zero, and the day appears first in the
286             format, then the day number will be removed from output.
287         '''
288         str = time.strftime(format, (self.year, self.month, self.day,
289             self.hour, self.minute, self.second, 0, 0, 0))
290         # handle zero day by removing it
291         if format.startswith('%d') and str[0] == '0':
292             return ' ' + str[1:]
293         return str
295     def __repr__(self):
296         return '<Date %s>'%self.__str__()
298     def local(self, offset):
299         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
300         """
301         return Date((self.year, self.month, self.day, self.hour + offset,
302             self.minute, self.second, 0, 0, 0))
304     def get_tuple(self):
305         return (self.year, self.month, self.day, self.hour, self.minute,
306             self.second, 0, 0, 0)
308     def serialise(self):
309         return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
310             self.day, self.hour, self.minute, self.second)
312 class Interval:
313     '''
314     Date intervals are specified using the suffixes "y", "m", and "d". The
315     suffix "w" (for "week") means 7 days. Time intervals are specified in
316     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
317     may not).
319       "3y" means three years
320       "2y 1m" means two years and one month
321       "1m 25d" means one month and 25 days
322       "2w 3d" means two weeks and three days
323       "1d 2:50" means one day, two hours, and 50 minutes
324       "14:00" means 14 hours
325       "0:04:33" means four minutes and 33 seconds
327     Example usage:
328         >>> Interval("  3w  1  d  2:00")
329         <Interval + 22d 2:00>
330         >>> Date(". + 2d") + Interval("- 3w")
331         <Date 2000-06-07.00:34:02>
332         >>> Interval('1:59:59') + Interval('00:00:01')
333         <Interval + 2:00>
334         >>> Interval('2:00') + Interval('- 00:00:01')
335         <Interval + 1:59:59>
336         >>> Interval('1y')/2
337         <Interval + 6m>
338         >>> Interval('1:00')/2
339         <Interval + 0:30>
340         >>> Interval('2003-03-18')
341         <Interval + [number of days between now and 2003-03-18]>
342         >>> Interval('-4d 2003-03-18')
343         <Interval + [number of days between now and 2003-03-14]>
345     Interval arithmetic is handled in a couple of special ways, trying
346     to cater for the most common cases. Fundamentally, Intervals which
347     have both date and time parts will result in strange results in
348     arithmetic - because of the impossibility of handling day->month->year
349     over- and under-flows. Intervals may also be divided by some number.
351     Intervals are added to Dates in order of:
352        seconds, minutes, hours, years, months, days
354     Calculations involving months (eg '+2m') have no effect on days - only
355     days (or over/underflow from hours/mins/secs) will do that, and
356     days-per-month and leap years are accounted for. Leap seconds are not.
358     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
359     minute, second) is the serialisation format returned by the serialise()
360     method, and is accepted as an argument on instatiation.
362     TODO: more examples, showing the order of addition operation
363     '''
364     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
365         """Construct an interval given a specification."""
366         if type(spec) == type(''):
367             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
368         else:
369             if len(spec) == 7:
370                 self.sign, self.year, self.month, self.day, self.hour, \
371                     self.minute, self.second = spec
372             else:
373                 # old, buggy spec form
374                 self.sign = sign
375                 self.year, self.month, self.day, self.hour, self.minute, \
376                     self.second = spec
378     def set(self, spec, allowdate=1, interval_re=re.compile('''
379             \s*(?P<s>[-+])?         # + or -
380             \s*((?P<y>\d+\s*)y)?    # year
381             \s*((?P<m>\d+\s*)m)?    # month
382             \s*((?P<w>\d+\s*)w)?    # week
383             \s*((?P<d>\d+\s*)d)?    # day
384             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
385             \s*(?P<D>
386                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
387                  \.?                                       # .
388                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
389                )?''', re.VERBOSE), serialised_re=re.compile('''
390             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
391             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
392             add_granularity=0):
393         ''' set the date to the value in spec
394         '''
395         self.year = self.month = self.week = self.day = self.hour = \
396             self.minute = self.second = 0
397         self.sign = 1
398         m = serialised_re.match(spec)
399         if not m:
400             m = interval_re.match(spec)
401             if not m:
402                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
403                     '[#d] [[[H]H:MM]:SS] [date spec]')
404         else:
405             allowdate = 0
407         # pull out all the info specified
408         info = m.groupdict()
409         if add_granularity:
410             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
412         valid = 0
413         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
414                 'H':'hour', 'M':'minute', 'S':'second'}.items():
415             if info.get(group, None) is not None:
416                 valid = 1
417                 setattr(self, attr, int(info[group]))
419         # make sure it's valid
420         if not valid and not info['D']:
421             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
422                 '[#d] [[[H]H:MM]:SS]')
424         if self.week:
425             self.day = self.day + self.week*7
427         if info['s'] is not None:
428             self.sign = {'+':1, '-':-1}[info['s']]
430         # use a date spec if one is given
431         if allowdate and info['D'] is not None:
432             now = Date('.')
433             date = Date(info['D'])
434             # if no time part was specified, nuke it in the "now" date
435             if not date.hour or date.minute or date.second:
436                 now.hour = now.minute = now.second = 0
437             if date != now:
438                 y = now - (date + self)
439                 self.__init__(y.get_tuple())
441     def __cmp__(self, other):
442         """Compare this interval to another interval."""
443         if other is None:
444             # we are always larger than None
445             return 1
446         for attr in 'sign year month day hour minute second'.split():
447             r = cmp(getattr(self, attr), getattr(other, attr))
448             if r:
449                 return r
450         return 0
452     def __str__(self):
453         """Return this interval as a string."""
454         l = []
455         if self.year: l.append('%sy'%self.year)
456         if self.month: l.append('%sm'%self.month)
457         if self.day: l.append('%sd'%self.day)
458         if self.second:
459             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
460         elif self.hour or self.minute:
461             l.append('%d:%02d'%(self.hour, self.minute))
462         if l:
463             l.insert(0, {1:'+', -1:'-'}[self.sign])
464         return ' '.join(l)
466     def __add__(self, other):
467         if isinstance(other, Date):
468             # the other is a Date - produce a Date
469             return Date(other.addInterval(self))
470         elif isinstance(other, Interval):
471             # add the other Interval to this one
472             a = self.get_tuple()
473             as = a[0]
474             b = other.get_tuple()
475             bs = b[0]
476             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
477             i.insert(0, 1)
478             i = fixTimeOverflow(i)
479             return Interval(i)
480         # nope, no idea what to do with this other...
481         raise TypeError, "Can't add %r"%other
483     def __sub__(self, other):
484         if isinstance(other, Date):
485             # the other is a Date - produce a Date
486             interval = Interval(self.get_tuple())
487             interval.sign *= -1
488             return Date(other.addInterval(interval))
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 __div__(self, other):
503         ''' Divide this interval by an int value.
505             Can't divide years and months sensibly in the _same_
506             calculation as days/time, so raise an error in that situation.
507         '''
508         try:
509             other = float(other)
510         except TypeError:
511             raise ValueError, "Can only divide Intervals by numbers"
513         y, m, d, H, M, S = (self.year, self.month, self.day,
514             self.hour, self.minute, self.second)
515         if y or m:
516             if d or H or M or S:
517                 raise ValueError, "Can't divide Interval with date and time"
518             months = self.year*12 + self.month
519             months *= self.sign
521             months = int(months/other)
523             sign = months<0 and -1 or 1
524             m = months%12
525             y = months / 12
526             return Interval((sign, y, m, 0, 0, 0, 0))
528         else:
529             # handle a day/time division
530             seconds = S + M*60 + H*60*60 + d*60*60*24
531             seconds *= self.sign
533             seconds = int(seconds/other)
535             sign = seconds<0 and -1 or 1
536             seconds *= sign
537             S = seconds%60
538             seconds /= 60
539             M = seconds%60
540             seconds /= 60
541             H = seconds%24
542             d = seconds / 24
543             return Interval((sign, 0, 0, d, H, M, S))
545     def __repr__(self):
546         return '<Interval %s>'%self.__str__()
548     def pretty(self):
549         ''' print up the date date using one of these nice formats..
550         '''
551         if self.year:
552             if self.year == 1:
553                 return _('1 year')
554             else:
555                 return _('%(number)s years')%{'number': self.year}
556         elif self.month or self.day > 13:
557             days = (self.month * 30) + self.day
558             if days > 28:
559                 if int(days/30) > 1:
560                     s = _('%(number)s months')%{'number': int(days/30)}
561                 else:
562                     s = _('1 month')
563             else:
564                 s = _('%(number)s weeks')%{'number': int(days/7)}
565         elif self.day > 7:
566             s = _('1 week')
567         elif self.day > 1:
568             s = _('%(number)s days')%{'number': self.day}
569         elif self.day == 1 or self.hour > 12:
570             if self.sign > 0:
571                 return _('tomorrow')
572             else:
573                 return _('yesterday')
574         elif self.hour > 1:
575             s = _('%(number)s hours')%{'number': self.hour}
576         elif self.hour == 1:
577             if self.minute < 15:
578                 s = _('an hour')
579             elif self.minute/15 == 2:
580                 s = _('1 1/2 hours')
581             else:
582                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
583         elif self.minute < 1:
584             if self.sign > 0:
585                 return _('in a moment')
586             else:
587                 return _('just now')
588         elif self.minute == 1:
589             s = _('1 minute')
590         elif self.minute < 15:
591             s = _('%(number)s minutes')%{'number': self.minute}
592         elif int(self.minute/15) == 2:
593             s = _('1/2 an hour')
594         else:
595             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
596         if self.sign < 0: 
597             s = s + _(' ago')
598         else:
599             s = _('in') + s
600         return s
602     def get_tuple(self):
603         return (self.sign, self.year, self.month, self.day, self.hour,
604             self.minute, self.second)
606     def serialise(self):
607         sign = self.sign > 0 and '+' or '-'
608         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
609             self.day, self.hour, self.minute, self.second)
611 def fixTimeOverflow(time):
612     ''' Handle the overflow in the time portion (H, M, S) of "time":
613             (sign, y,m,d,H,M,S)
615         Overflow and underflow will at most affect the _days_ portion of
616         the date. We do not overflow days to months as we don't know _how_
617         to, generally.
618     '''
619     # XXX we could conceivably use this function for handling regular dates
620     # XXX too - we just need to interrogate the month/year for the day
621     # XXX overflow...
623     sign, y, m, d, H, M, S = time
624     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
625     if seconds:
626         sign = seconds<0 and -1 or 1
627         seconds *= sign
628         S = seconds%60
629         seconds /= 60
630         M = seconds%60
631         seconds /= 60
632         H = seconds%24
633         d = seconds / 24
634     else:
635         months = y*12 + m
636         sign = months<0 and -1 or 1
637         months *= sign
638         m = months%12
639         y = months/12
641     return (sign, y, m, d, H, M, S)
643 class Range:
644     """
645     Represents range between two values
646     Ranges can be created using one of theese two alternative syntaxes:
647         
648         1. Native english syntax: 
649             [[From] <value>][ To <value>]
650            Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
652         2. "Geek" syntax:
653             [<value>][; <value>]
655     Either first or second <value> can be omitted in both syntaxes.
657     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003):
658         >>> Range("from 2-12 to 4-2")
659         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
660         
661         >>> Range("18:00 TO +2m")
662         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
663         
664         >>> Range("12:00")
665         <Range from 2003-03-08.12:00:00 to None>
666         
667         >>> Range("tO +3d")
668         <Range from None to 2003-03-11.20:07:48>
669         
670         >>> Range("2002-11-10; 2002-12-12")
671         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
672         
673         >>> Range("; 20:00 +1d")
674         <Range from None to 2003-03-09.20:00:00>
676     """
677     def __init__(self, spec, Type, allow_granularity=1, **params):
678         """Initializes Range of type <Type> from given <spec> string.
679         
680         Sets two properties - from_value and to_value. None assigned to any of
681         this properties means "infinitum" (-infinitum to from_value and
682         +infinitum to to_value)
684         The Type parameter here should be class itself (e.g. Date), not a
685         class instance.
686         
687         """
688         self.range_type = Type
689         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
690         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
691         # Check which syntax to use
692         if  spec.find(';') == -1:
693             # Native english
694             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
695         else:
696             # Geek
697             mch_range = re.search(re_geek_range, spec.strip())
698         if mch_range:
699             self.from_value, self.to_value = mch_range.groups()
700             if self.from_value:
701                 self.from_value = Type(self.from_value.strip(), **params)
702             if self.to_value:
703                 self.to_value = Type(self.to_value.strip(), **params)
704         else:
705             if allow_granularity:
706                 self.from_value = Type(spec, **params)
707                 self.to_value = Type(spec, add_granularity=1, **params)
708             else:
709                 raise ValueError, "Invalid range"
711     def __str__(self):
712         return "from %s to %s" % (self.from_value, self.to_value)
714     def __repr__(self):
715         return "<Range %s>" % self.__str__()
716  
717 def test_range():
718     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
719         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
720     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
721     for rspec in rspecs:
722         print '>>> Range("%s")' % rspec
723         print `Range(rspec, Date)`
724         print
725     for rspec in rispecs:
726         print '>>> Range("%s")' % rspec
727         print `Range(rspec, Interval)`
728         print
730 def test():
731     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
732     for interval in intervals:
733         print '>>> Interval("%s")'%interval
734         print `Interval(interval)`
736     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
737         "08-13.22:13", "14:25", '2002-12')
738     for date in dates:
739         print '>>> Date("%s")'%date
740         print `Date(date)`
742     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
743     for date, interval in sums:
744         print '>>> Date("%s") + Interval("%s")'%(date, interval)
745         print `Date(date) + Interval(interval)`
747 if __name__ == '__main__':
748     test()
750 # vim: set filetype=python ts=4 sw=4 et si