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.60 2004-02-11 23:55:08 richard Exp $
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
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.
54 Examples::
56 "2000-04-17" means <Date 2000-04-17.00:00:00>
57 "01-25" means <Date yyyy-01-25.00:00:00>
58 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
59 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
60 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
61 "14:25" means <Date yyyy-mm-dd.19:25:00>
62 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
63 "2003" means <Date 2003-01-01.00:00:00>
64 "2003-06" means <Date 2003-06-01.00:00:00>
65 "." means "right now"
67 The Date class should understand simple date expressions of the form
68 stamp + interval and stamp - interval. When adding or subtracting
69 intervals involving months or years, the components are handled
70 separately. For example, when evaluating "2000-06-25 + 1m 10d", we
71 first add one month to get 2000-07-25, then add 10 days to get
72 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
73 or 41 days). 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'
99 is a full date or a partial form, with an optional added or
100 subtracted interval. Or a date 9-tuple.
101 'offset'
102 is the local time zone offset from GMT in hours.
103 """
104 if type(spec) == type(''):
105 self.set(spec, offset=offset, add_granularity=add_granularity)
106 else:
107 y,m,d,H,M,S,x,x,x = spec
108 ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
109 self.year, self.month, self.day, self.hour, self.minute, \
110 self.second, x, x, x = time.gmtime(ts)
112 usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS][offset]'
113 def set(self, spec, offset=0, date_re=re.compile(r'''
114 ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
115 |(?P<a>\d\d?)[/-](?P<b>\d\d?))? # or mm-dd
116 (?P<n>\.)? # .
117 (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # hh:mm:ss
118 (?P<o>.+)? # offset
119 ''', re.VERBOSE), serialised_re=re.compile(r'''
120 (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
121 ''', re.VERBOSE), add_granularity=0):
122 ''' set the date to the value in spec
123 '''
125 m = serialised_re.match(spec)
126 if m is not None:
127 # we're serialised - easy!
128 self.year, self.month, self.day, self.hour, self.minute, \
129 self.second = map(int, m.groups()[:6])
130 return
132 # not serialised data, try usual format
133 m = date_re.match(spec)
134 if m is None:
135 raise ValueError, _('Not a date spec: %s' % self.usagespec)
137 info = m.groupdict()
139 if add_granularity:
140 _add_granularity(info, 'SMHdmyab')
142 # get the current date as our default
143 y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
145 if info['y'] is not None or info['a'] is not None:
146 if info['y'] is not None:
147 y = int(info['y'])
148 m,d = (1,1)
149 if info['m'] is not None:
150 m = int(info['m'])
151 if info['d'] is not None:
152 d = int(info['d'])
153 if info['a'] is not None:
154 m = int(info['a'])
155 d = int(info['b'])
156 H = -offset
157 M = S = 0
159 # override hour, minute, second parts
160 if info['H'] is not None and info['M'] is not None:
161 H = int(info['H']) - offset
162 M = int(info['M'])
163 S = 0
164 if info['S'] is not None: S = int(info['S'])
166 if add_granularity:
167 S = S - 1
169 # now handle the adjustment of hour
170 ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
171 self.year, self.month, self.day, self.hour, self.minute, \
172 self.second, x, x, x = time.gmtime(ts)
174 if info.get('o', None):
175 try:
176 self.applyInterval(Interval(info['o'], allowdate=0))
177 except ValueError:
178 raise ValueError, _('Not a date spec: %s' % self.usagespec)
180 def addInterval(self, interval):
181 ''' Add the interval to this date, returning the date tuple
182 '''
183 # do the basic calc
184 sign = interval.sign
185 year = self.year + sign * interval.year
186 month = self.month + sign * interval.month
187 day = self.day + sign * interval.day
188 hour = self.hour + sign * interval.hour
189 minute = self.minute + sign * interval.minute
190 second = self.second + sign * interval.second
192 # now cope with under- and over-flow
193 # first do the time
194 while (second < 0 or second > 59 or minute < 0 or minute > 59 or
195 hour < 0 or hour > 23):
196 if second < 0: minute -= 1; second += 60
197 elif second > 59: minute += 1; second -= 60
198 if minute < 0: hour -= 1; minute += 60
199 elif minute > 59: hour += 1; minute -= 60
200 if hour < 0: day -= 1; hour += 24
201 elif hour > 23: day += 1; hour -= 24
203 # fix up the month so we're within range
204 while month < 1 or month > 12:
205 if month < 1: year -= 1; month += 12
206 if month > 12: year += 1; month -= 12
208 # now do the days, now that we know what month we're in
209 def get_mdays(year, month):
210 if month == 2 and calendar.isleap(year): return 29
211 else: return calendar.mdays[month]
213 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
214 # now to day under/over
215 if day < 1:
216 # When going backwards, decrement month, then increment days
217 month -= 1
218 day += get_mdays(year,month)
219 elif day > get_mdays(year,month):
220 # When going forwards, decrement days, then increment month
221 day -= get_mdays(year,month)
222 month += 1
224 # possibly fix up the month so we're within range
225 while month < 1 or month > 12:
226 if month < 1: year -= 1; month += 12 ; day += 31
227 if month > 12: year += 1; month -= 12
229 return (year, month, day, hour, minute, second, 0, 0, 0)
231 def differenceDate(self, other):
232 "Return the difference between this date and another date"
234 def applyInterval(self, interval):
235 ''' Apply the interval to this date
236 '''
237 self.year, self.month, self.day, self.hour, self.minute, \
238 self.second, x, x, x = self.addInterval(interval)
240 def __add__(self, interval):
241 """Add an interval to this date to produce another date.
242 """
243 return Date(self.addInterval(interval))
245 # deviates from spec to allow subtraction of dates as well
246 def __sub__(self, other):
247 """ Subtract:
248 1. an interval from this date to produce another date.
249 2. a date from this date to produce an interval.
250 """
251 if isinstance(other, Interval):
252 other = Interval(other.get_tuple())
253 other.sign *= -1
254 return self.__add__(other)
256 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
258 return self.dateDelta(other)
260 def dateDelta(self, other):
261 """ Produce an Interval of the difference between this date
262 and another date. Only returns days:hours:minutes:seconds.
263 """
264 # Returning intervals larger than a day is almost
265 # impossible - months, years, weeks, are all so imprecise.
266 a = calendar.timegm((self.year, self.month, self.day, self.hour,
267 self.minute, self.second, 0, 0, 0))
268 b = calendar.timegm((other.year, other.month, other.day,
269 other.hour, other.minute, other.second, 0, 0, 0))
270 diff = a - b
271 if diff > 0:
272 sign = 1
273 else:
274 sign = -1
275 diff = -diff
276 S = diff%60
277 M = (diff/60)%60
278 H = (diff/(60*60))%24
279 d = diff/(24*60*60)
280 return Interval((0, 0, d, H, M, S), sign=sign)
282 def __cmp__(self, other):
283 """Compare this date to another date."""
284 if other is None:
285 return 1
286 for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
287 if not hasattr(other, attr):
288 return 1
289 r = cmp(getattr(self, attr), getattr(other, attr))
290 if r: return r
291 return 0
293 def __str__(self):
294 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
295 return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
296 self.hour, self.minute, self.second)
298 def pretty(self, format='%d %B %Y'):
299 ''' print up the date date using a pretty format...
301 Note that if the day is zero, and the day appears first in the
302 format, then the day number will be removed from output.
303 '''
304 str = time.strftime(format, (self.year, self.month, self.day,
305 self.hour, self.minute, self.second, 0, 0, 0))
306 # handle zero day by removing it
307 if format.startswith('%d') and str[0] == '0':
308 return ' ' + str[1:]
309 return str
311 def __repr__(self):
312 return '<Date %s>'%self.__str__()
314 def local(self, offset):
315 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
316 """
317 return Date((self.year, self.month, self.day, self.hour + offset,
318 self.minute, self.second, 0, 0, 0))
320 def get_tuple(self):
321 return (self.year, self.month, self.day, self.hour, self.minute,
322 self.second, 0, 0, 0)
324 def serialise(self):
325 return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
326 self.day, self.hour, self.minute, self.second)
328 def timestamp(self):
329 ''' return a UNIX timestamp for this date '''
330 return calendar.timegm((self.year, self.month, self.day, self.hour,
331 self.minute, self.second, 0, 0, 0))
333 class Interval:
334 '''
335 Date intervals are specified using the suffixes "y", "m", and "d". The
336 suffix "w" (for "week") means 7 days. Time intervals are specified in
337 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
338 may not).
340 "3y" means three years
341 "2y 1m" means two years and one month
342 "1m 25d" means one month and 25 days
343 "2w 3d" means two weeks and three days
344 "1d 2:50" means one day, two hours, and 50 minutes
345 "14:00" means 14 hours
346 "0:04:33" means four minutes and 33 seconds
348 Example usage:
349 >>> Interval(" 3w 1 d 2:00")
350 <Interval + 22d 2:00>
351 >>> Date(". + 2d") + Interval("- 3w")
352 <Date 2000-06-07.00:34:02>
353 >>> Interval('1:59:59') + Interval('00:00:01')
354 <Interval + 2:00>
355 >>> Interval('2:00') + Interval('- 00:00:01')
356 <Interval + 1:59:59>
357 >>> Interval('1y')/2
358 <Interval + 6m>
359 >>> Interval('1:00')/2
360 <Interval + 0:30>
361 >>> Interval('2003-03-18')
362 <Interval + [number of days between now and 2003-03-18]>
363 >>> Interval('-4d 2003-03-18')
364 <Interval + [number of days between now and 2003-03-14]>
366 Interval arithmetic is handled in a couple of special ways, trying
367 to cater for the most common cases. Fundamentally, Intervals which
368 have both date and time parts will result in strange results in
369 arithmetic - because of the impossibility of handling day->month->year
370 over- and under-flows. Intervals may also be divided by some number.
372 Intervals are added to Dates in order of:
373 seconds, minutes, hours, years, months, days
375 Calculations involving months (eg '+2m') have no effect on days - only
376 days (or over/underflow from hours/mins/secs) will do that, and
377 days-per-month and leap years are accounted for. Leap seconds are not.
379 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
380 minute, second) is the serialisation format returned by the serialise()
381 method, and is accepted as an argument on instatiation.
383 TODO: more examples, showing the order of addition operation
384 '''
385 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
386 """Construct an interval given a specification."""
387 if type(spec) == type(''):
388 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
389 else:
390 if len(spec) == 7:
391 self.sign, self.year, self.month, self.day, self.hour, \
392 self.minute, self.second = spec
393 else:
394 # old, buggy spec form
395 self.sign = sign
396 self.year, self.month, self.day, self.hour, self.minute, \
397 self.second = spec
399 def set(self, spec, allowdate=1, interval_re=re.compile('''
400 \s*(?P<s>[-+])? # + or -
401 \s*((?P<y>\d+\s*)y)? # year
402 \s*((?P<m>\d+\s*)m)? # month
403 \s*((?P<w>\d+\s*)w)? # week
404 \s*((?P<d>\d+\s*)d)? # day
405 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
406 \s*(?P<D>
407 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
408 \.? # .
409 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
410 )?''', re.VERBOSE), serialised_re=re.compile('''
411 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
412 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
413 add_granularity=0):
414 ''' set the date to the value in spec
415 '''
416 self.year = self.month = self.week = self.day = self.hour = \
417 self.minute = self.second = 0
418 self.sign = 1
419 m = serialised_re.match(spec)
420 if not m:
421 m = interval_re.match(spec)
422 if not m:
423 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
424 '[#d] [[[H]H:MM]:SS] [date spec]')
425 else:
426 allowdate = 0
428 # pull out all the info specified
429 info = m.groupdict()
430 if add_granularity:
431 _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
433 valid = 0
434 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
435 'H':'hour', 'M':'minute', 'S':'second'}.items():
436 if info.get(group, None) is not None:
437 valid = 1
438 setattr(self, attr, int(info[group]))
440 # make sure it's valid
441 if not valid and not info['D']:
442 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
443 '[#d] [[[H]H:MM]:SS]')
445 if self.week:
446 self.day = self.day + self.week*7
448 if info['s'] is not None:
449 self.sign = {'+':1, '-':-1}[info['s']]
451 # use a date spec if one is given
452 if allowdate and info['D'] is not None:
453 now = Date('.')
454 date = Date(info['D'])
455 # if no time part was specified, nuke it in the "now" date
456 if not date.hour or date.minute or date.second:
457 now.hour = now.minute = now.second = 0
458 if date != now:
459 y = now - (date + self)
460 self.__init__(y.get_tuple())
462 def __cmp__(self, other):
463 """Compare this interval to another interval."""
464 if other is None:
465 # we are always larger than None
466 return 1
467 for attr in 'sign year month day hour minute second'.split():
468 r = cmp(getattr(self, attr), getattr(other, attr))
469 if r:
470 return r
471 return 0
473 def __str__(self):
474 """Return this interval as a string."""
475 l = []
476 if self.year: l.append('%sy'%self.year)
477 if self.month: l.append('%sm'%self.month)
478 if self.day: l.append('%sd'%self.day)
479 if self.second:
480 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
481 elif self.hour or self.minute:
482 l.append('%d:%02d'%(self.hour, self.minute))
483 if l:
484 l.insert(0, {1:'+', -1:'-'}[self.sign])
485 return ' '.join(l)
487 def __add__(self, other):
488 if isinstance(other, Date):
489 # the other is a Date - produce a Date
490 return Date(other.addInterval(self))
491 elif isinstance(other, Interval):
492 # add the other Interval to this one
493 a = self.get_tuple()
494 as = a[0]
495 b = other.get_tuple()
496 bs = b[0]
497 i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
498 i.insert(0, 1)
499 i = fixTimeOverflow(i)
500 return Interval(i)
501 # nope, no idea what to do with this other...
502 raise TypeError, "Can't add %r"%other
504 def __sub__(self, other):
505 if isinstance(other, Date):
506 # the other is a Date - produce a Date
507 interval = Interval(self.get_tuple())
508 interval.sign *= -1
509 return Date(other.addInterval(interval))
510 elif isinstance(other, Interval):
511 # add the other Interval to this one
512 a = self.get_tuple()
513 as = a[0]
514 b = other.get_tuple()
515 bs = b[0]
516 i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
517 i.insert(0, 1)
518 i = fixTimeOverflow(i)
519 return Interval(i)
520 # nope, no idea what to do with this other...
521 raise TypeError, "Can't add %r"%other
523 def __div__(self, other):
524 """ Divide this interval by an int value.
526 Can't divide years and months sensibly in the _same_
527 calculation as days/time, so raise an error in that situation.
528 """
529 try:
530 other = float(other)
531 except TypeError:
532 raise ValueError, "Can only divide Intervals by numbers"
534 y, m, d, H, M, S = (self.year, self.month, self.day,
535 self.hour, self.minute, self.second)
536 if y or m:
537 if d or H or M or S:
538 raise ValueError, "Can't divide Interval with date and time"
539 months = self.year*12 + self.month
540 months *= self.sign
542 months = int(months/other)
544 sign = months<0 and -1 or 1
545 m = months%12
546 y = months / 12
547 return Interval((sign, y, m, 0, 0, 0, 0))
549 else:
550 # handle a day/time division
551 seconds = S + M*60 + H*60*60 + d*60*60*24
552 seconds *= self.sign
554 seconds = int(seconds/other)
556 sign = seconds<0 and -1 or 1
557 seconds *= sign
558 S = seconds%60
559 seconds /= 60
560 M = seconds%60
561 seconds /= 60
562 H = seconds%24
563 d = seconds / 24
564 return Interval((sign, 0, 0, d, H, M, S))
566 def __repr__(self):
567 return '<Interval %s>'%self.__str__()
569 def pretty(self):
570 ''' print up the date date using one of these nice formats..
571 '''
572 if self.year:
573 if self.year == 1:
574 s = _('1 year')
575 else:
576 s = _('%(number)s years')%{'number': self.year}
577 elif self.month or self.day > 13:
578 days = (self.month * 30) + self.day
579 if days > 28:
580 if int(days/30) > 1:
581 s = _('%(number)s months')%{'number': int(days/30)}
582 else:
583 s = _('1 month')
584 else:
585 s = _('%(number)s weeks')%{'number': int(days/7)}
586 elif self.day > 7:
587 s = _('1 week')
588 elif self.day > 1:
589 s = _('%(number)s days')%{'number': self.day}
590 elif self.day == 1 or self.hour > 12:
591 if self.sign > 0:
592 return _('tomorrow')
593 else:
594 return _('yesterday')
595 elif self.hour > 1:
596 s = _('%(number)s hours')%{'number': self.hour}
597 elif self.hour == 1:
598 if self.minute < 15:
599 s = _('an hour')
600 elif self.minute/15 == 2:
601 s = _('1 1/2 hours')
602 else:
603 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
604 elif self.minute < 1:
605 if self.sign > 0:
606 return _('in a moment')
607 else:
608 return _('just now')
609 elif self.minute == 1:
610 s = _('1 minute')
611 elif self.minute < 15:
612 s = _('%(number)s minutes')%{'number': self.minute}
613 elif int(self.minute/15) == 2:
614 s = _('1/2 an hour')
615 else:
616 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
617 if self.sign < 0:
618 s = s + _(' ago')
619 else:
620 s = _('in ') + s
621 return s
623 def get_tuple(self):
624 return (self.sign, self.year, self.month, self.day, self.hour,
625 self.minute, self.second)
627 def serialise(self):
628 sign = self.sign > 0 and '+' or '-'
629 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
630 self.day, self.hour, self.minute, self.second)
632 def fixTimeOverflow(time):
633 """ Handle the overflow in the time portion (H, M, S) of "time":
634 (sign, y,m,d,H,M,S)
636 Overflow and underflow will at most affect the _days_ portion of
637 the date. We do not overflow days to months as we don't know _how_
638 to, generally.
639 """
640 # XXX we could conceivably use this function for handling regular dates
641 # XXX too - we just need to interrogate the month/year for the day
642 # XXX overflow...
644 sign, y, m, d, H, M, S = time
645 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
646 if seconds:
647 sign = seconds<0 and -1 or 1
648 seconds *= sign
649 S = seconds%60
650 seconds /= 60
651 M = seconds%60
652 seconds /= 60
653 H = seconds%24
654 d = seconds / 24
655 else:
656 months = y*12 + m
657 sign = months<0 and -1 or 1
658 months *= sign
659 m = months%12
660 y = months/12
662 return (sign, y, m, d, H, M, S)
664 class Range:
665 """Represents range between two values
666 Ranges can be created using one of theese two alternative syntaxes:
668 1. Native english syntax::
670 [[From] <value>][ To <value>]
672 Keywords "From" and "To" are case insensitive. Keyword "From" is
673 optional.
675 2. "Geek" syntax::
677 [<value>][; <value>]
679 Either first or second <value> can be omitted in both syntaxes.
681 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
683 >>> Range("from 2-12 to 4-2")
684 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
686 >>> Range("18:00 TO +2m")
687 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
689 >>> Range("12:00")
690 <Range from 2003-03-08.12:00:00 to None>
692 >>> Range("tO +3d")
693 <Range from None to 2003-03-11.20:07:48>
695 >>> Range("2002-11-10; 2002-12-12")
696 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
698 >>> Range("; 20:00 +1d")
699 <Range from None to 2003-03-09.20:00:00>
701 """
702 def __init__(self, spec, Type, allow_granularity=1, **params):
703 """Initializes Range of type <Type> from given <spec> string.
705 Sets two properties - from_value and to_value. None assigned to any of
706 this properties means "infinitum" (-infinitum to from_value and
707 +infinitum to to_value)
709 The Type parameter here should be class itself (e.g. Date), not a
710 class instance.
712 """
713 self.range_type = Type
714 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
715 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
716 # Check which syntax to use
717 if spec.find(';') == -1:
718 # Native english
719 mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
720 else:
721 # Geek
722 mch_range = re.search(re_geek_range, spec.strip())
723 if mch_range:
724 self.from_value, self.to_value = mch_range.groups()
725 if self.from_value:
726 self.from_value = Type(self.from_value.strip(), **params)
727 if self.to_value:
728 self.to_value = Type(self.to_value.strip(), **params)
729 else:
730 if allow_granularity:
731 self.from_value = Type(spec, **params)
732 self.to_value = Type(spec, add_granularity=1, **params)
733 else:
734 raise ValueError, "Invalid range"
736 def __str__(self):
737 return "from %s to %s" % (self.from_value, self.to_value)
739 def __repr__(self):
740 return "<Range %s>" % self.__str__()
742 def test_range():
743 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
744 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
745 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
746 for rspec in rspecs:
747 print '>>> Range("%s")' % rspec
748 print `Range(rspec, Date)`
749 print
750 for rspec in rispecs:
751 print '>>> Range("%s")' % rspec
752 print `Range(rspec, Interval)`
753 print
755 def test():
756 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
757 for interval in intervals:
758 print '>>> Interval("%s")'%interval
759 print `Interval(interval)`
761 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
762 "08-13.22:13", "14:25", '2002-12')
763 for date in dates:
764 print '>>> Date("%s")'%date
765 print `Date(date)`
767 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
768 for date, interval in sums:
769 print '>>> Date("%s") + Interval("%s")'%(date, interval)
770 print `Date(date) + Interval(interval)`
772 if __name__ == '__main__':
773 test()
775 # vim: set filetype=python ts=4 sw=4 et si