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.55 2003-11-03 10:23:05 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 applyInterval(self, interval):
230 ''' Apply the interval to this date
231 '''
232 self.year, self.month, self.day, self.hour, self.minute, \
233 self.second, x, x, x = self.addInterval(interval)
235 def __add__(self, interval):
236 """Add an interval to this date to produce another date.
237 """
238 return Date(self.addInterval(interval))
240 # deviates from spec to allow subtraction of dates as well
241 def __sub__(self, other):
242 """ Subtract:
243 1. an interval from this date to produce another date.
244 2. a date from this date to produce an interval.
245 """
246 if isinstance(other, Interval):
247 other = Interval(other.get_tuple())
248 other.sign *= -1
249 return self.__add__(other)
251 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
253 # TODO this code will fall over laughing if the dates cross
254 # leap years, phases of the moon, ....
255 a = calendar.timegm((self.year, self.month, self.day, self.hour,
256 self.minute, self.second, 0, 0, 0))
257 b = calendar.timegm((other.year, other.month, other.day,
258 other.hour, other.minute, other.second, 0, 0, 0))
259 diff = a - b
260 if diff < 0:
261 sign = 1
262 diff = -diff
263 else:
264 sign = -1
265 S = diff%60
266 M = (diff/60)%60
267 H = (diff/(60*60))%60
268 if H>1: S = 0
269 d = (diff/(24*60*60))%30
270 if d>1: H = S = M = 0
271 m = (diff/(30*24*60*60))%12
272 if m>1: H = S = M = 0
273 y = (diff/(365*24*60*60))
274 if y>1: d = H = S = M = 0
275 return Interval((y, m, d, H, M, S), sign=sign)
277 def __cmp__(self, other):
278 """Compare this date to another date."""
279 if other is None:
280 return 1
281 for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
282 if not hasattr(other, attr):
283 return 1
284 r = cmp(getattr(self, attr), getattr(other, attr))
285 if r: return r
286 return 0
288 def __str__(self):
289 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
290 return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
291 self.hour, self.minute, self.second)
293 def pretty(self, format='%d %B %Y'):
294 ''' print up the date date using a pretty format...
296 Note that if the day is zero, and the day appears first in the
297 format, then the day number will be removed from output.
298 '''
299 str = time.strftime(format, (self.year, self.month, self.day,
300 self.hour, self.minute, self.second, 0, 0, 0))
301 # handle zero day by removing it
302 if format.startswith('%d') and str[0] == '0':
303 return ' ' + str[1:]
304 return str
306 def __repr__(self):
307 return '<Date %s>'%self.__str__()
309 def local(self, offset):
310 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
311 """
312 return Date((self.year, self.month, self.day, self.hour + offset,
313 self.minute, self.second, 0, 0, 0))
315 def get_tuple(self):
316 return (self.year, self.month, self.day, self.hour, self.minute,
317 self.second, 0, 0, 0)
319 def serialise(self):
320 return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
321 self.day, self.hour, self.minute, self.second)
323 class Interval:
324 '''
325 Date intervals are specified using the suffixes "y", "m", and "d". The
326 suffix "w" (for "week") means 7 days. Time intervals are specified in
327 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
328 may not).
330 "3y" means three years
331 "2y 1m" means two years and one month
332 "1m 25d" means one month and 25 days
333 "2w 3d" means two weeks and three days
334 "1d 2:50" means one day, two hours, and 50 minutes
335 "14:00" means 14 hours
336 "0:04:33" means four minutes and 33 seconds
338 Example usage:
339 >>> Interval(" 3w 1 d 2:00")
340 <Interval + 22d 2:00>
341 >>> Date(". + 2d") + Interval("- 3w")
342 <Date 2000-06-07.00:34:02>
343 >>> Interval('1:59:59') + Interval('00:00:01')
344 <Interval + 2:00>
345 >>> Interval('2:00') + Interval('- 00:00:01')
346 <Interval + 1:59:59>
347 >>> Interval('1y')/2
348 <Interval + 6m>
349 >>> Interval('1:00')/2
350 <Interval + 0:30>
351 >>> Interval('2003-03-18')
352 <Interval + [number of days between now and 2003-03-18]>
353 >>> Interval('-4d 2003-03-18')
354 <Interval + [number of days between now and 2003-03-14]>
356 Interval arithmetic is handled in a couple of special ways, trying
357 to cater for the most common cases. Fundamentally, Intervals which
358 have both date and time parts will result in strange results in
359 arithmetic - because of the impossibility of handling day->month->year
360 over- and under-flows. Intervals may also be divided by some number.
362 Intervals are added to Dates in order of:
363 seconds, minutes, hours, years, months, days
365 Calculations involving months (eg '+2m') have no effect on days - only
366 days (or over/underflow from hours/mins/secs) will do that, and
367 days-per-month and leap years are accounted for. Leap seconds are not.
369 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
370 minute, second) is the serialisation format returned by the serialise()
371 method, and is accepted as an argument on instatiation.
373 TODO: more examples, showing the order of addition operation
374 '''
375 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
376 """Construct an interval given a specification."""
377 if type(spec) == type(''):
378 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
379 else:
380 if len(spec) == 7:
381 self.sign, self.year, self.month, self.day, self.hour, \
382 self.minute, self.second = spec
383 else:
384 # old, buggy spec form
385 self.sign = sign
386 self.year, self.month, self.day, self.hour, self.minute, \
387 self.second = spec
389 def set(self, spec, allowdate=1, interval_re=re.compile('''
390 \s*(?P<s>[-+])? # + or -
391 \s*((?P<y>\d+\s*)y)? # year
392 \s*((?P<m>\d+\s*)m)? # month
393 \s*((?P<w>\d+\s*)w)? # week
394 \s*((?P<d>\d+\s*)d)? # day
395 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
396 \s*(?P<D>
397 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
398 \.? # .
399 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
400 )?''', re.VERBOSE), serialised_re=re.compile('''
401 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
402 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
403 add_granularity=0):
404 ''' set the date to the value in spec
405 '''
406 self.year = self.month = self.week = self.day = self.hour = \
407 self.minute = self.second = 0
408 self.sign = 1
409 m = serialised_re.match(spec)
410 if not m:
411 m = interval_re.match(spec)
412 if not m:
413 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
414 '[#d] [[[H]H:MM]:SS] [date spec]')
415 else:
416 allowdate = 0
418 # pull out all the info specified
419 info = m.groupdict()
420 if add_granularity:
421 _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
423 valid = 0
424 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
425 'H':'hour', 'M':'minute', 'S':'second'}.items():
426 if info.get(group, None) is not None:
427 valid = 1
428 setattr(self, attr, int(info[group]))
430 # make sure it's valid
431 if not valid and not info['D']:
432 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
433 '[#d] [[[H]H:MM]:SS]')
435 if self.week:
436 self.day = self.day + self.week*7
438 if info['s'] is not None:
439 self.sign = {'+':1, '-':-1}[info['s']]
441 # use a date spec if one is given
442 if allowdate and info['D'] is not None:
443 now = Date('.')
444 date = Date(info['D'])
445 # if no time part was specified, nuke it in the "now" date
446 if not date.hour or date.minute or date.second:
447 now.hour = now.minute = now.second = 0
448 if date != now:
449 y = now - (date + self)
450 self.__init__(y.get_tuple())
452 def __cmp__(self, other):
453 """Compare this interval to another interval."""
454 if other is None:
455 # we are always larger than None
456 return 1
457 for attr in 'sign year month day hour minute second'.split():
458 r = cmp(getattr(self, attr), getattr(other, attr))
459 if r:
460 return r
461 return 0
463 def __str__(self):
464 """Return this interval as a string."""
465 l = []
466 if self.year: l.append('%sy'%self.year)
467 if self.month: l.append('%sm'%self.month)
468 if self.day: l.append('%sd'%self.day)
469 if self.second:
470 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
471 elif self.hour or self.minute:
472 l.append('%d:%02d'%(self.hour, self.minute))
473 if l:
474 l.insert(0, {1:'+', -1:'-'}[self.sign])
475 return ' '.join(l)
477 def __add__(self, other):
478 if isinstance(other, Date):
479 # the other is a Date - produce a Date
480 return Date(other.addInterval(self))
481 elif isinstance(other, Interval):
482 # add the other Interval to this one
483 a = self.get_tuple()
484 as = a[0]
485 b = other.get_tuple()
486 bs = b[0]
487 i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
488 i.insert(0, 1)
489 i = fixTimeOverflow(i)
490 return Interval(i)
491 # nope, no idea what to do with this other...
492 raise TypeError, "Can't add %r"%other
494 def __sub__(self, other):
495 if isinstance(other, Date):
496 # the other is a Date - produce a Date
497 interval = Interval(self.get_tuple())
498 interval.sign *= -1
499 return Date(other.addInterval(interval))
500 elif isinstance(other, Interval):
501 # add the other Interval to this one
502 a = self.get_tuple()
503 as = a[0]
504 b = other.get_tuple()
505 bs = b[0]
506 i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
507 i.insert(0, 1)
508 i = fixTimeOverflow(i)
509 return Interval(i)
510 # nope, no idea what to do with this other...
511 raise TypeError, "Can't add %r"%other
513 def __div__(self, other):
514 ''' Divide this interval by an int value.
516 Can't divide years and months sensibly in the _same_
517 calculation as days/time, so raise an error in that situation.
518 '''
519 try:
520 other = float(other)
521 except TypeError:
522 raise ValueError, "Can only divide Intervals by numbers"
524 y, m, d, H, M, S = (self.year, self.month, self.day,
525 self.hour, self.minute, self.second)
526 if y or m:
527 if d or H or M or S:
528 raise ValueError, "Can't divide Interval with date and time"
529 months = self.year*12 + self.month
530 months *= self.sign
532 months = int(months/other)
534 sign = months<0 and -1 or 1
535 m = months%12
536 y = months / 12
537 return Interval((sign, y, m, 0, 0, 0, 0))
539 else:
540 # handle a day/time division
541 seconds = S + M*60 + H*60*60 + d*60*60*24
542 seconds *= self.sign
544 seconds = int(seconds/other)
546 sign = seconds<0 and -1 or 1
547 seconds *= sign
548 S = seconds%60
549 seconds /= 60
550 M = seconds%60
551 seconds /= 60
552 H = seconds%24
553 d = seconds / 24
554 return Interval((sign, 0, 0, d, H, M, S))
556 def __repr__(self):
557 return '<Interval %s>'%self.__str__()
559 def pretty(self):
560 ''' print up the date date using one of these nice formats..
561 '''
562 if self.year:
563 if self.year == 1:
564 return _('1 year')
565 else:
566 return _('%(number)s years')%{'number': self.year}
567 elif self.month or self.day > 13:
568 days = (self.month * 30) + self.day
569 if days > 28:
570 if int(days/30) > 1:
571 s = _('%(number)s months')%{'number': int(days/30)}
572 else:
573 s = _('1 month')
574 else:
575 s = _('%(number)s weeks')%{'number': int(days/7)}
576 elif self.day > 7:
577 s = _('1 week')
578 elif self.day > 1:
579 s = _('%(number)s days')%{'number': self.day}
580 elif self.day == 1 or self.hour > 12:
581 if self.sign > 0:
582 return _('tomorrow')
583 else:
584 return _('yesterday')
585 elif self.hour > 1:
586 s = _('%(number)s hours')%{'number': self.hour}
587 elif self.hour == 1:
588 if self.minute < 15:
589 s = _('an hour')
590 elif self.minute/15 == 2:
591 s = _('1 1/2 hours')
592 else:
593 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
594 elif self.minute < 1:
595 if self.sign > 0:
596 return _('in a moment')
597 else:
598 return _('just now')
599 elif self.minute == 1:
600 s = _('1 minute')
601 elif self.minute < 15:
602 s = _('%(number)s minutes')%{'number': self.minute}
603 elif int(self.minute/15) == 2:
604 s = _('1/2 an hour')
605 else:
606 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
607 if self.sign < 0:
608 s = s + _(' ago')
609 else:
610 s = _('in ') + s
611 return s
613 def get_tuple(self):
614 return (self.sign, self.year, self.month, self.day, self.hour,
615 self.minute, self.second)
617 def serialise(self):
618 sign = self.sign > 0 and '+' or '-'
619 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
620 self.day, self.hour, self.minute, self.second)
622 def fixTimeOverflow(time):
623 ''' Handle the overflow in the time portion (H, M, S) of "time":
624 (sign, y,m,d,H,M,S)
626 Overflow and underflow will at most affect the _days_ portion of
627 the date. We do not overflow days to months as we don't know _how_
628 to, generally.
629 '''
630 # XXX we could conceivably use this function for handling regular dates
631 # XXX too - we just need to interrogate the month/year for the day
632 # XXX overflow...
634 sign, y, m, d, H, M, S = time
635 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
636 if seconds:
637 sign = seconds<0 and -1 or 1
638 seconds *= sign
639 S = seconds%60
640 seconds /= 60
641 M = seconds%60
642 seconds /= 60
643 H = seconds%24
644 d = seconds / 24
645 else:
646 months = y*12 + m
647 sign = months<0 and -1 or 1
648 months *= sign
649 m = months%12
650 y = months/12
652 return (sign, y, m, d, H, M, S)
654 class Range:
655 """
656 Represents range between two values
657 Ranges can be created using one of theese two alternative syntaxes:
659 1. Native english syntax:
660 [[From] <value>][ To <value>]
661 Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
663 2. "Geek" syntax:
664 [<value>][; <value>]
666 Either first or second <value> can be omitted in both syntaxes.
668 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003):
669 >>> Range("from 2-12 to 4-2")
670 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
672 >>> Range("18:00 TO +2m")
673 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
675 >>> Range("12:00")
676 <Range from 2003-03-08.12:00:00 to None>
678 >>> Range("tO +3d")
679 <Range from None to 2003-03-11.20:07:48>
681 >>> Range("2002-11-10; 2002-12-12")
682 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
684 >>> Range("; 20:00 +1d")
685 <Range from None to 2003-03-09.20:00:00>
687 """
688 def __init__(self, spec, Type, allow_granularity=1, **params):
689 """Initializes Range of type <Type> from given <spec> string.
691 Sets two properties - from_value and to_value. None assigned to any of
692 this properties means "infinitum" (-infinitum to from_value and
693 +infinitum to to_value)
695 The Type parameter here should be class itself (e.g. Date), not a
696 class instance.
698 """
699 self.range_type = Type
700 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
701 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
702 # Check which syntax to use
703 if spec.find(';') == -1:
704 # Native english
705 mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
706 else:
707 # Geek
708 mch_range = re.search(re_geek_range, spec.strip())
709 if mch_range:
710 self.from_value, self.to_value = mch_range.groups()
711 if self.from_value:
712 self.from_value = Type(self.from_value.strip(), **params)
713 if self.to_value:
714 self.to_value = Type(self.to_value.strip(), **params)
715 else:
716 if allow_granularity:
717 self.from_value = Type(spec, **params)
718 self.to_value = Type(spec, add_granularity=1, **params)
719 else:
720 raise ValueError, "Invalid range"
722 def __str__(self):
723 return "from %s to %s" % (self.from_value, self.to_value)
725 def __repr__(self):
726 return "<Range %s>" % self.__str__()
728 def test_range():
729 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
730 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
731 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
732 for rspec in rspecs:
733 print '>>> Range("%s")' % rspec
734 print `Range(rspec, Date)`
735 print
736 for rspec in rispecs:
737 print '>>> Range("%s")' % rspec
738 print `Range(rspec, Interval)`
739 print
741 def test():
742 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
743 for interval in intervals:
744 print '>>> Interval("%s")'%interval
745 print `Interval(interval)`
747 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
748 "08-13.22:13", "14:25", '2002-12')
749 for date in dates:
750 print '>>> Date("%s")'%date
751 print `Date(date)`
753 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
754 for date, interval in sums:
755 print '>>> Date("%s") + Interval("%s")'%(date, interval)
756 print `Date(date) + Interval(interval)`
758 if __name__ == '__main__':
759 test()
761 # vim: set filetype=python ts=4 sw=4 et si