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.63 2004-03-24 04:57:25 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 return
107 elif hasattr(spec, 'tuple'):
108 spec = spec.tuple()
109 try:
110 y,m,d,H,M,S,x,x,x = spec
111 frac = S - int(S)
112 ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
113 self.year, self.month, self.day, self.hour, self.minute, \
114 self.second, x, x, x = time.gmtime(ts)
115 # we lost the fractional part
116 self.second = self.second + frac
117 except:
118 raise ValueError, 'Unknown spec %r'%spec
120 usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS.SSS][offset]'
121 def set(self, spec, offset=0, date_re=re.compile(r'''
122 ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
123 |(?P<a>\d\d?)[/-](?P<b>\d\d?))? # or mm-dd
124 (?P<n>\.)? # .
125 (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d(\.\d+)?))?)? # hh:mm:ss
126 (?P<o>.+)? # offset
127 ''', re.VERBOSE), serialised_re=re.compile(r'''
128 (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d(\.\d+)?)
129 ''', re.VERBOSE), add_granularity=0):
130 ''' set the date to the value in spec
131 '''
133 m = serialised_re.match(spec)
134 if m is not None:
135 # we're serialised - easy!
136 g = m.groups()
137 (self.year, self.month, self.day, self.hour, self.minute) = \
138 map(int, g[:5])
139 self.second = float(g[5])
140 return
142 # not serialised data, try usual format
143 m = date_re.match(spec)
144 if m is None:
145 raise ValueError, _('Not a date spec: %s' % self.usagespec)
147 info = m.groupdict()
149 if add_granularity:
150 _add_granularity(info, 'SMHdmyab')
152 # get the current date as our default
153 ts = time.time()
154 frac = ts - int(ts)
155 y,m,d,H,M,S,x,x,x = time.gmtime(ts)
156 # gmtime loses the fractional seconds
157 S = S + frac
159 if info['y'] is not None or info['a'] is not None:
160 if info['y'] is not None:
161 y = int(info['y'])
162 m,d = (1,1)
163 if info['m'] is not None:
164 m = int(info['m'])
165 if info['d'] is not None:
166 d = int(info['d'])
167 if info['a'] is not None:
168 m = int(info['a'])
169 d = int(info['b'])
170 H = -offset
171 M = S = 0
173 # override hour, minute, second parts
174 if info['H'] is not None and info['M'] is not None:
175 H = int(info['H']) - offset
176 M = int(info['M'])
177 S = 0
178 if info['S'] is not None:
179 S = float(info['S'])
181 if add_granularity:
182 S = S - 1
184 # now handle the adjustment of hour
185 frac = S - int(S)
186 ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
187 self.year, self.month, self.day, self.hour, self.minute, \
188 self.second, x, x, x = time.gmtime(ts)
189 # we lost the fractional part along the way
190 self.second = self.second + frac
192 if info.get('o', None):
193 try:
194 self.applyInterval(Interval(info['o'], allowdate=0))
195 except ValueError:
196 raise ValueError, _('%r not a date spec (%s)')%(spec,
197 self.usagespec)
199 def addInterval(self, interval):
200 ''' Add the interval to this date, returning the date tuple
201 '''
202 # do the basic calc
203 sign = interval.sign
204 year = self.year + sign * interval.year
205 month = self.month + sign * interval.month
206 day = self.day + sign * interval.day
207 hour = self.hour + sign * interval.hour
208 minute = self.minute + sign * interval.minute
209 # Intervals work on whole seconds
210 second = int(self.second) + sign * interval.second
212 # now cope with under- and over-flow
213 # first do the time
214 while (second < 0 or second > 59 or minute < 0 or minute > 59 or
215 hour < 0 or hour > 23):
216 if second < 0: minute -= 1; second += 60
217 elif second > 59: minute += 1; second -= 60
218 if minute < 0: hour -= 1; minute += 60
219 elif minute > 59: hour += 1; minute -= 60
220 if hour < 0: day -= 1; hour += 24
221 elif hour > 23: day += 1; hour -= 24
223 # fix up the month so we're within range
224 while month < 1 or month > 12:
225 if month < 1: year -= 1; month += 12
226 if month > 12: year += 1; month -= 12
228 # now do the days, now that we know what month we're in
229 def get_mdays(year, month):
230 if month == 2 and calendar.isleap(year): return 29
231 else: return calendar.mdays[month]
233 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
234 # now to day under/over
235 if day < 1:
236 # When going backwards, decrement month, then increment days
237 month -= 1
238 day += get_mdays(year,month)
239 elif day > get_mdays(year,month):
240 # When going forwards, decrement days, then increment month
241 day -= get_mdays(year,month)
242 month += 1
244 # possibly fix up the month so we're within range
245 while month < 1 or month > 12:
246 if month < 1: year -= 1; month += 12 ; day += 31
247 if month > 12: year += 1; month -= 12
249 return (year, month, day, hour, minute, second, 0, 0, 0)
251 def differenceDate(self, other):
252 "Return the difference between this date and another date"
254 def applyInterval(self, interval):
255 ''' Apply the interval to this date
256 '''
257 self.year, self.month, self.day, self.hour, self.minute, \
258 self.second, x, x, x = self.addInterval(interval)
260 def __add__(self, interval):
261 """Add an interval to this date to produce another date.
262 """
263 return Date(self.addInterval(interval))
265 # deviates from spec to allow subtraction of dates as well
266 def __sub__(self, other):
267 """ Subtract:
268 1. an interval from this date to produce another date.
269 2. a date from this date to produce an interval.
270 """
271 if isinstance(other, Interval):
272 other = Interval(other.get_tuple())
273 other.sign *= -1
274 return self.__add__(other)
276 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
278 return self.dateDelta(other)
280 def dateDelta(self, other):
281 """ Produce an Interval of the difference between this date
282 and another date. Only returns days:hours:minutes:seconds.
283 """
284 # Returning intervals larger than a day is almost
285 # impossible - months, years, weeks, are all so imprecise.
286 a = calendar.timegm((self.year, self.month, self.day, self.hour,
287 self.minute, self.second, 0, 0, 0))
288 b = calendar.timegm((other.year, other.month, other.day,
289 other.hour, other.minute, other.second, 0, 0, 0))
290 # intervals work in whole seconds
291 diff = int(a - b)
292 if diff > 0:
293 sign = 1
294 else:
295 sign = -1
296 diff = -diff
297 S = diff%60
298 M = (diff/60)%60
299 H = (diff/(60*60))%24
300 d = diff/(24*60*60)
301 return Interval((0, 0, d, H, M, S), sign=sign)
303 def __cmp__(self, other, int_seconds=0):
304 """Compare this date to another date."""
305 if other is None:
306 return 1
307 for attr in ('year', 'month', 'day', 'hour', 'minute'):
308 if not hasattr(other, attr):
309 return 1
310 r = cmp(getattr(self, attr), getattr(other, attr))
311 if r: return r
312 if not hasattr(other, 'second'):
313 return 1
314 if int_seconds:
315 return cmp(int(self.second), int(other.second))
316 return cmp(self.second, other.second)
318 def __str__(self):
319 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
320 return self.formal()
322 def formal(self, sep='.', sec='%02d'):
323 f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
324 return f%(self.year, self.month, self.day, self.hour, self.minute,
325 self.second)
327 def pretty(self, format='%d %B %Y'):
328 ''' print up the date date using a pretty format...
330 Note that if the day is zero, and the day appears first in the
331 format, then the day number will be removed from output.
332 '''
333 str = time.strftime(format, (self.year, self.month, self.day,
334 self.hour, self.minute, self.second, 0, 0, 0))
335 # handle zero day by removing it
336 if format.startswith('%d') and str[0] == '0':
337 return ' ' + str[1:]
338 return str
340 def __repr__(self):
341 return '<Date %s>'%self.formal(sec='%f')
343 def local(self, offset):
344 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
345 """
346 return Date((self.year, self.month, self.day, self.hour + offset,
347 self.minute, self.second, 0, 0, 0))
349 def get_tuple(self):
350 return (self.year, self.month, self.day, self.hour, self.minute,
351 self.second, 0, 0, 0)
353 def serialise(self):
354 return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
355 self.day, self.hour, self.minute, self.second)
357 def timestamp(self):
358 ''' return a UNIX timestamp for this date '''
359 frac = self.second - int(self.second)
360 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
361 self.minute, self.second, 0, 0, 0))
362 # we lose the fractional part
363 return ts + frac
365 class Interval:
366 '''
367 Date intervals are specified using the suffixes "y", "m", and "d". The
368 suffix "w" (for "week") means 7 days. Time intervals are specified in
369 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
370 may not).
372 "3y" means three years
373 "2y 1m" means two years and one month
374 "1m 25d" means one month and 25 days
375 "2w 3d" means two weeks and three days
376 "1d 2:50" means one day, two hours, and 50 minutes
377 "14:00" means 14 hours
378 "0:04:33" means four minutes and 33 seconds
380 Example usage:
381 >>> Interval(" 3w 1 d 2:00")
382 <Interval + 22d 2:00>
383 >>> Date(". + 2d") + Interval("- 3w")
384 <Date 2000-06-07.00:34:02>
385 >>> Interval('1:59:59') + Interval('00:00:01')
386 <Interval + 2:00>
387 >>> Interval('2:00') + Interval('- 00:00:01')
388 <Interval + 1:59:59>
389 >>> Interval('1y')/2
390 <Interval + 6m>
391 >>> Interval('1:00')/2
392 <Interval + 0:30>
393 >>> Interval('2003-03-18')
394 <Interval + [number of days between now and 2003-03-18]>
395 >>> Interval('-4d 2003-03-18')
396 <Interval + [number of days between now and 2003-03-14]>
398 Interval arithmetic is handled in a couple of special ways, trying
399 to cater for the most common cases. Fundamentally, Intervals which
400 have both date and time parts will result in strange results in
401 arithmetic - because of the impossibility of handling day->month->year
402 over- and under-flows. Intervals may also be divided by some number.
404 Intervals are added to Dates in order of:
405 seconds, minutes, hours, years, months, days
407 Calculations involving months (eg '+2m') have no effect on days - only
408 days (or over/underflow from hours/mins/secs) will do that, and
409 days-per-month and leap years are accounted for. Leap seconds are not.
411 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
412 minute, second) is the serialisation format returned by the serialise()
413 method, and is accepted as an argument on instatiation.
415 TODO: more examples, showing the order of addition operation
416 '''
417 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
418 """Construct an interval given a specification."""
419 if type(spec) == type(''):
420 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
421 else:
422 if len(spec) == 7:
423 self.sign, self.year, self.month, self.day, self.hour, \
424 self.minute, self.second = spec
425 self.second = int(self.second)
426 else:
427 # old, buggy spec form
428 self.sign = sign
429 self.year, self.month, self.day, self.hour, self.minute, \
430 self.second = spec
431 self.second = int(self.second)
433 def set(self, spec, allowdate=1, interval_re=re.compile('''
434 \s*(?P<s>[-+])? # + or -
435 \s*((?P<y>\d+\s*)y)? # year
436 \s*((?P<m>\d+\s*)m)? # month
437 \s*((?P<w>\d+\s*)w)? # week
438 \s*((?P<d>\d+\s*)d)? # day
439 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
440 \s*(?P<D>
441 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
442 \.? # .
443 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
444 )?''', re.VERBOSE), serialised_re=re.compile('''
445 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
446 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
447 add_granularity=0):
448 ''' set the date to the value in spec
449 '''
450 self.year = self.month = self.week = self.day = self.hour = \
451 self.minute = self.second = 0
452 self.sign = 1
453 m = serialised_re.match(spec)
454 if not m:
455 m = interval_re.match(spec)
456 if not m:
457 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
458 '[#d] [[[H]H:MM]:SS] [date spec]')
459 else:
460 allowdate = 0
462 # pull out all the info specified
463 info = m.groupdict()
464 if add_granularity:
465 _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
467 valid = 0
468 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
469 'H':'hour', 'M':'minute', 'S':'second'}.items():
470 if info.get(group, None) is not None:
471 valid = 1
472 setattr(self, attr, int(info[group]))
474 # make sure it's valid
475 if not valid and not info['D']:
476 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
477 '[#d] [[[H]H:MM]:SS]')
479 if self.week:
480 self.day = self.day + self.week*7
482 if info['s'] is not None:
483 self.sign = {'+':1, '-':-1}[info['s']]
485 # use a date spec if one is given
486 if allowdate and info['D'] is not None:
487 now = Date('.')
488 date = Date(info['D'])
489 # if no time part was specified, nuke it in the "now" date
490 if not date.hour or date.minute or date.second:
491 now.hour = now.minute = now.second = 0
492 if date != now:
493 y = now - (date + self)
494 self.__init__(y.get_tuple())
496 def __cmp__(self, other):
497 """Compare this interval to another interval."""
498 if other is None:
499 # we are always larger than None
500 return 1
501 for attr in 'sign year month day hour minute second'.split():
502 r = cmp(getattr(self, attr), getattr(other, attr))
503 if r:
504 return r
505 return 0
507 def __str__(self):
508 """Return this interval as a string."""
509 l = []
510 if self.year: l.append('%sy'%self.year)
511 if self.month: l.append('%sm'%self.month)
512 if self.day: l.append('%sd'%self.day)
513 if self.second:
514 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
515 elif self.hour or self.minute:
516 l.append('%d:%02d'%(self.hour, self.minute))
517 if l:
518 l.insert(0, {1:'+', -1:'-'}[self.sign])
519 return ' '.join(l)
521 def __add__(self, other):
522 if isinstance(other, Date):
523 # the other is a Date - produce a Date
524 return Date(other.addInterval(self))
525 elif isinstance(other, Interval):
526 # add the other Interval to this one
527 a = self.get_tuple()
528 as = a[0]
529 b = other.get_tuple()
530 bs = b[0]
531 i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
532 i.insert(0, 1)
533 i = fixTimeOverflow(i)
534 return Interval(i)
535 # nope, no idea what to do with this other...
536 raise TypeError, "Can't add %r"%other
538 def __sub__(self, other):
539 if isinstance(other, Date):
540 # the other is a Date - produce a Date
541 interval = Interval(self.get_tuple())
542 interval.sign *= -1
543 return Date(other.addInterval(interval))
544 elif isinstance(other, Interval):
545 # add the other Interval to this one
546 a = self.get_tuple()
547 as = a[0]
548 b = other.get_tuple()
549 bs = b[0]
550 i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
551 i.insert(0, 1)
552 i = fixTimeOverflow(i)
553 return Interval(i)
554 # nope, no idea what to do with this other...
555 raise TypeError, "Can't add %r"%other
557 def __div__(self, other):
558 """ Divide this interval by an int value.
560 Can't divide years and months sensibly in the _same_
561 calculation as days/time, so raise an error in that situation.
562 """
563 try:
564 other = float(other)
565 except TypeError:
566 raise ValueError, "Can only divide Intervals by numbers"
568 y, m, d, H, M, S = (self.year, self.month, self.day,
569 self.hour, self.minute, self.second)
570 if y or m:
571 if d or H or M or S:
572 raise ValueError, "Can't divide Interval with date and time"
573 months = self.year*12 + self.month
574 months *= self.sign
576 months = int(months/other)
578 sign = months<0 and -1 or 1
579 m = months%12
580 y = months / 12
581 return Interval((sign, y, m, 0, 0, 0, 0))
583 else:
584 # handle a day/time division
585 seconds = S + M*60 + H*60*60 + d*60*60*24
586 seconds *= self.sign
588 seconds = int(seconds/other)
590 sign = seconds<0 and -1 or 1
591 seconds *= sign
592 S = seconds%60
593 seconds /= 60
594 M = seconds%60
595 seconds /= 60
596 H = seconds%24
597 d = seconds / 24
598 return Interval((sign, 0, 0, d, H, M, S))
600 def __repr__(self):
601 return '<Interval %s>'%self.__str__()
603 def pretty(self):
604 ''' print up the date date using one of these nice formats..
605 '''
606 if self.year:
607 if self.year == 1:
608 s = _('1 year')
609 else:
610 s = _('%(number)s years')%{'number': self.year}
611 elif self.month or self.day > 13:
612 days = (self.month * 30) + self.day
613 if days > 28:
614 if int(days/30) > 1:
615 s = _('%(number)s months')%{'number': int(days/30)}
616 else:
617 s = _('1 month')
618 else:
619 s = _('%(number)s weeks')%{'number': int(days/7)}
620 elif self.day > 7:
621 s = _('1 week')
622 elif self.day > 1:
623 s = _('%(number)s days')%{'number': self.day}
624 elif self.day == 1 or self.hour > 12:
625 if self.sign > 0:
626 return _('tomorrow')
627 else:
628 return _('yesterday')
629 elif self.hour > 1:
630 s = _('%(number)s hours')%{'number': self.hour}
631 elif self.hour == 1:
632 if self.minute < 15:
633 s = _('an hour')
634 elif self.minute/15 == 2:
635 s = _('1 1/2 hours')
636 else:
637 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
638 elif self.minute < 1:
639 if self.sign > 0:
640 return _('in a moment')
641 else:
642 return _('just now')
643 elif self.minute == 1:
644 s = _('1 minute')
645 elif self.minute < 15:
646 s = _('%(number)s minutes')%{'number': self.minute}
647 elif int(self.minute/15) == 2:
648 s = _('1/2 an hour')
649 else:
650 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
651 if self.sign < 0:
652 s = s + _(' ago')
653 else:
654 s = _('in ') + s
655 return s
657 def get_tuple(self):
658 return (self.sign, self.year, self.month, self.day, self.hour,
659 self.minute, self.second)
661 def serialise(self):
662 sign = self.sign > 0 and '+' or '-'
663 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
664 self.day, self.hour, self.minute, self.second)
666 def fixTimeOverflow(time):
667 """ Handle the overflow in the time portion (H, M, S) of "time":
668 (sign, y,m,d,H,M,S)
670 Overflow and underflow will at most affect the _days_ portion of
671 the date. We do not overflow days to months as we don't know _how_
672 to, generally.
673 """
674 # XXX we could conceivably use this function for handling regular dates
675 # XXX too - we just need to interrogate the month/year for the day
676 # XXX overflow...
678 sign, y, m, d, H, M, S = time
679 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
680 if seconds:
681 sign = seconds<0 and -1 or 1
682 seconds *= sign
683 S = seconds%60
684 seconds /= 60
685 M = seconds%60
686 seconds /= 60
687 H = seconds%24
688 d = seconds / 24
689 else:
690 months = y*12 + m
691 sign = months<0 and -1 or 1
692 months *= sign
693 m = months%12
694 y = months/12
696 return (sign, y, m, d, H, M, S)
698 class Range:
699 """Represents range between two values
700 Ranges can be created using one of theese two alternative syntaxes:
702 1. Native english syntax::
704 [[From] <value>][ To <value>]
706 Keywords "From" and "To" are case insensitive. Keyword "From" is
707 optional.
709 2. "Geek" syntax::
711 [<value>][; <value>]
713 Either first or second <value> can be omitted in both syntaxes.
715 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
717 >>> Range("from 2-12 to 4-2")
718 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
720 >>> Range("18:00 TO +2m")
721 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
723 >>> Range("12:00")
724 <Range from 2003-03-08.12:00:00 to None>
726 >>> Range("tO +3d")
727 <Range from None to 2003-03-11.20:07:48>
729 >>> Range("2002-11-10; 2002-12-12")
730 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
732 >>> Range("; 20:00 +1d")
733 <Range from None to 2003-03-09.20:00:00>
735 """
736 def __init__(self, spec, Type, allow_granularity=1, **params):
737 """Initializes Range of type <Type> from given <spec> string.
739 Sets two properties - from_value and to_value. None assigned to any of
740 this properties means "infinitum" (-infinitum to from_value and
741 +infinitum to to_value)
743 The Type parameter here should be class itself (e.g. Date), not a
744 class instance.
746 """
747 self.range_type = Type
748 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
749 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
750 # Check which syntax to use
751 if spec.find(';') == -1:
752 # Native english
753 mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
754 else:
755 # Geek
756 mch_range = re.search(re_geek_range, spec.strip())
757 if mch_range:
758 self.from_value, self.to_value = mch_range.groups()
759 if self.from_value:
760 self.from_value = Type(self.from_value.strip(), **params)
761 if self.to_value:
762 self.to_value = Type(self.to_value.strip(), **params)
763 else:
764 if allow_granularity:
765 self.from_value = Type(spec, **params)
766 self.to_value = Type(spec, add_granularity=1, **params)
767 else:
768 raise ValueError, "Invalid range"
770 def __str__(self):
771 return "from %s to %s" % (self.from_value, self.to_value)
773 def __repr__(self):
774 return "<Range %s>" % self.__str__()
776 def test_range():
777 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
778 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
779 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
780 for rspec in rspecs:
781 print '>>> Range("%s")' % rspec
782 print `Range(rspec, Date)`
783 print
784 for rspec in rispecs:
785 print '>>> Range("%s")' % rspec
786 print `Range(rspec, Interval)`
787 print
789 def test():
790 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
791 for interval in intervals:
792 print '>>> Interval("%s")'%interval
793 print `Interval(interval)`
795 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
796 "08-13.22:13", "14:25", '2002-12')
797 for date in dates:
798 print '>>> Date("%s")'%date
799 print `Date(date)`
801 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
802 for date, interval in sums:
803 print '>>> Date("%s") + Interval("%s")'%(date, interval)
804 print `Date(date) + Interval(interval)`
806 if __name__ == '__main__':
807 test()
809 # vim: set filetype=python ts=4 sw=4 et si