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
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:
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>
661 >>> Range("18:00 TO +2m")
662 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
664 >>> Range("12:00")
665 <Range from 2003-03-08.12:00:00 to None>
667 >>> Range("tO +3d")
668 <Range from None to 2003-03-11.20:07:48>
670 >>> Range("2002-11-10; 2002-12-12")
671 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
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.
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.
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__()
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