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 '''
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
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]
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:
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>
675 >>> Range("18:00 TO +2m")
676 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
678 >>> Range("12:00")
679 <Range from 2003-03-08.12:00:00 to None>
681 >>> Range("tO +3d")
682 <Range from None to 2003-03-11.20:07:48>
684 >>> Range("2002-11-10; 2002-12-12")
685 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
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.
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.
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__()
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