6b9a807e40f792ad3233a496efd225c3c8f444e8
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.62 2004-03-24 03:07:51 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):
304 """Compare this date to another date."""
305 if other is None:
306 return 1
307 for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
308 if not hasattr(other, attr):
309 return 1
310 r = cmp(getattr(self, attr), getattr(other, attr))
311 if r: return r
312 return 0
314 def __str__(self):
315 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
316 return self.formal()
318 def formal(self, sep='.', sec='%02d'):
319 f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
320 return f%(self.year, self.month, self.day, self.hour, self.minute,
321 self.second)
323 def pretty(self, format='%d %B %Y'):
324 ''' print up the date date using a pretty format...
326 Note that if the day is zero, and the day appears first in the
327 format, then the day number will be removed from output.
328 '''
329 str = time.strftime(format, (self.year, self.month, self.day,
330 self.hour, self.minute, self.second, 0, 0, 0))
331 # handle zero day by removing it
332 if format.startswith('%d') and str[0] == '0':
333 return ' ' + str[1:]
334 return str
336 def __repr__(self):
337 return '<Date %s>'%self.__str__()
339 def local(self, offset):
340 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
341 """
342 return Date((self.year, self.month, self.day, self.hour + offset,
343 self.minute, self.second, 0, 0, 0))
345 def get_tuple(self):
346 return (self.year, self.month, self.day, self.hour, self.minute,
347 self.second, 0, 0, 0)
349 def serialise(self):
350 return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
351 self.day, self.hour, self.minute, self.second)
353 def timestamp(self):
354 ''' return a UNIX timestamp for this date '''
355 frac = self.second - int(self.second)
356 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
357 self.minute, self.second, 0, 0, 0))
358 # we lose the fractional part
359 return ts + frac
361 class Interval:
362 '''
363 Date intervals are specified using the suffixes "y", "m", and "d". The
364 suffix "w" (for "week") means 7 days. Time intervals are specified in
365 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
366 may not).
368 "3y" means three years
369 "2y 1m" means two years and one month
370 "1m 25d" means one month and 25 days
371 "2w 3d" means two weeks and three days
372 "1d 2:50" means one day, two hours, and 50 minutes
373 "14:00" means 14 hours
374 "0:04:33" means four minutes and 33 seconds
376 Example usage:
377 >>> Interval(" 3w 1 d 2:00")
378 <Interval + 22d 2:00>
379 >>> Date(". + 2d") + Interval("- 3w")
380 <Date 2000-06-07.00:34:02>
381 >>> Interval('1:59:59') + Interval('00:00:01')
382 <Interval + 2:00>
383 >>> Interval('2:00') + Interval('- 00:00:01')
384 <Interval + 1:59:59>
385 >>> Interval('1y')/2
386 <Interval + 6m>
387 >>> Interval('1:00')/2
388 <Interval + 0:30>
389 >>> Interval('2003-03-18')
390 <Interval + [number of days between now and 2003-03-18]>
391 >>> Interval('-4d 2003-03-18')
392 <Interval + [number of days between now and 2003-03-14]>
394 Interval arithmetic is handled in a couple of special ways, trying
395 to cater for the most common cases. Fundamentally, Intervals which
396 have both date and time parts will result in strange results in
397 arithmetic - because of the impossibility of handling day->month->year
398 over- and under-flows. Intervals may also be divided by some number.
400 Intervals are added to Dates in order of:
401 seconds, minutes, hours, years, months, days
403 Calculations involving months (eg '+2m') have no effect on days - only
404 days (or over/underflow from hours/mins/secs) will do that, and
405 days-per-month and leap years are accounted for. Leap seconds are not.
407 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
408 minute, second) is the serialisation format returned by the serialise()
409 method, and is accepted as an argument on instatiation.
411 TODO: more examples, showing the order of addition operation
412 '''
413 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
414 """Construct an interval given a specification."""
415 if type(spec) == type(''):
416 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
417 else:
418 if len(spec) == 7:
419 self.sign, self.year, self.month, self.day, self.hour, \
420 self.minute, self.second = spec
421 self.second = int(self.second)
422 else:
423 # old, buggy spec form
424 self.sign = sign
425 self.year, self.month, self.day, self.hour, self.minute, \
426 self.second = spec
427 self.second = int(self.second)
429 def set(self, spec, allowdate=1, interval_re=re.compile('''
430 \s*(?P<s>[-+])? # + or -
431 \s*((?P<y>\d+\s*)y)? # year
432 \s*((?P<m>\d+\s*)m)? # month
433 \s*((?P<w>\d+\s*)w)? # week
434 \s*((?P<d>\d+\s*)d)? # day
435 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
436 \s*(?P<D>
437 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
438 \.? # .
439 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
440 )?''', re.VERBOSE), serialised_re=re.compile('''
441 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
442 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
443 add_granularity=0):
444 ''' set the date to the value in spec
445 '''
446 self.year = self.month = self.week = self.day = self.hour = \
447 self.minute = self.second = 0
448 self.sign = 1
449 m = serialised_re.match(spec)
450 if not m:
451 m = interval_re.match(spec)
452 if not m:
453 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
454 '[#d] [[[H]H:MM]:SS] [date spec]')
455 else:
456 allowdate = 0
458 # pull out all the info specified
459 info = m.groupdict()
460 if add_granularity:
461 _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
463 valid = 0
464 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
465 'H':'hour', 'M':'minute', 'S':'second'}.items():
466 if info.get(group, None) is not None:
467 valid = 1
468 setattr(self, attr, int(info[group]))
470 # make sure it's valid
471 if not valid and not info['D']:
472 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
473 '[#d] [[[H]H:MM]:SS]')
475 if self.week:
476 self.day = self.day + self.week*7
478 if info['s'] is not None:
479 self.sign = {'+':1, '-':-1}[info['s']]
481 # use a date spec if one is given
482 if allowdate and info['D'] is not None:
483 now = Date('.')
484 date = Date(info['D'])
485 # if no time part was specified, nuke it in the "now" date
486 if not date.hour or date.minute or date.second:
487 now.hour = now.minute = now.second = 0
488 if date != now:
489 y = now - (date + self)
490 self.__init__(y.get_tuple())
492 def __cmp__(self, other):
493 """Compare this interval to another interval."""
494 if other is None:
495 # we are always larger than None
496 return 1
497 for attr in 'sign year month day hour minute second'.split():
498 r = cmp(getattr(self, attr), getattr(other, attr))
499 if r:
500 return r
501 return 0
503 def __str__(self):
504 """Return this interval as a string."""
505 l = []
506 if self.year: l.append('%sy'%self.year)
507 if self.month: l.append('%sm'%self.month)
508 if self.day: l.append('%sd'%self.day)
509 if self.second:
510 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
511 elif self.hour or self.minute:
512 l.append('%d:%02d'%(self.hour, self.minute))
513 if l:
514 l.insert(0, {1:'+', -1:'-'}[self.sign])
515 return ' '.join(l)
517 def __add__(self, other):
518 if isinstance(other, Date):
519 # the other is a Date - produce a Date
520 return Date(other.addInterval(self))
521 elif isinstance(other, Interval):
522 # add the other Interval to this one
523 a = self.get_tuple()
524 as = a[0]
525 b = other.get_tuple()
526 bs = b[0]
527 i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
528 i.insert(0, 1)
529 i = fixTimeOverflow(i)
530 return Interval(i)
531 # nope, no idea what to do with this other...
532 raise TypeError, "Can't add %r"%other
534 def __sub__(self, other):
535 if isinstance(other, Date):
536 # the other is a Date - produce a Date
537 interval = Interval(self.get_tuple())
538 interval.sign *= -1
539 return Date(other.addInterval(interval))
540 elif isinstance(other, Interval):
541 # add the other Interval to this one
542 a = self.get_tuple()
543 as = a[0]
544 b = other.get_tuple()
545 bs = b[0]
546 i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
547 i.insert(0, 1)
548 i = fixTimeOverflow(i)
549 return Interval(i)
550 # nope, no idea what to do with this other...
551 raise TypeError, "Can't add %r"%other
553 def __div__(self, other):
554 """ Divide this interval by an int value.
556 Can't divide years and months sensibly in the _same_
557 calculation as days/time, so raise an error in that situation.
558 """
559 try:
560 other = float(other)
561 except TypeError:
562 raise ValueError, "Can only divide Intervals by numbers"
564 y, m, d, H, M, S = (self.year, self.month, self.day,
565 self.hour, self.minute, self.second)
566 if y or m:
567 if d or H or M or S:
568 raise ValueError, "Can't divide Interval with date and time"
569 months = self.year*12 + self.month
570 months *= self.sign
572 months = int(months/other)
574 sign = months<0 and -1 or 1
575 m = months%12
576 y = months / 12
577 return Interval((sign, y, m, 0, 0, 0, 0))
579 else:
580 # handle a day/time division
581 seconds = S + M*60 + H*60*60 + d*60*60*24
582 seconds *= self.sign
584 seconds = int(seconds/other)
586 sign = seconds<0 and -1 or 1
587 seconds *= sign
588 S = seconds%60
589 seconds /= 60
590 M = seconds%60
591 seconds /= 60
592 H = seconds%24
593 d = seconds / 24
594 return Interval((sign, 0, 0, d, H, M, S))
596 def __repr__(self):
597 return '<Interval %s>'%self.__str__()
599 def pretty(self):
600 ''' print up the date date using one of these nice formats..
601 '''
602 if self.year:
603 if self.year == 1:
604 s = _('1 year')
605 else:
606 s = _('%(number)s years')%{'number': self.year}
607 elif self.month or self.day > 13:
608 days = (self.month * 30) + self.day
609 if days > 28:
610 if int(days/30) > 1:
611 s = _('%(number)s months')%{'number': int(days/30)}
612 else:
613 s = _('1 month')
614 else:
615 s = _('%(number)s weeks')%{'number': int(days/7)}
616 elif self.day > 7:
617 s = _('1 week')
618 elif self.day > 1:
619 s = _('%(number)s days')%{'number': self.day}
620 elif self.day == 1 or self.hour > 12:
621 if self.sign > 0:
622 return _('tomorrow')
623 else:
624 return _('yesterday')
625 elif self.hour > 1:
626 s = _('%(number)s hours')%{'number': self.hour}
627 elif self.hour == 1:
628 if self.minute < 15:
629 s = _('an hour')
630 elif self.minute/15 == 2:
631 s = _('1 1/2 hours')
632 else:
633 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
634 elif self.minute < 1:
635 if self.sign > 0:
636 return _('in a moment')
637 else:
638 return _('just now')
639 elif self.minute == 1:
640 s = _('1 minute')
641 elif self.minute < 15:
642 s = _('%(number)s minutes')%{'number': self.minute}
643 elif int(self.minute/15) == 2:
644 s = _('1/2 an hour')
645 else:
646 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
647 if self.sign < 0:
648 s = s + _(' ago')
649 else:
650 s = _('in ') + s
651 return s
653 def get_tuple(self):
654 return (self.sign, self.year, self.month, self.day, self.hour,
655 self.minute, self.second)
657 def serialise(self):
658 sign = self.sign > 0 and '+' or '-'
659 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
660 self.day, self.hour, self.minute, self.second)
662 def fixTimeOverflow(time):
663 """ Handle the overflow in the time portion (H, M, S) of "time":
664 (sign, y,m,d,H,M,S)
666 Overflow and underflow will at most affect the _days_ portion of
667 the date. We do not overflow days to months as we don't know _how_
668 to, generally.
669 """
670 # XXX we could conceivably use this function for handling regular dates
671 # XXX too - we just need to interrogate the month/year for the day
672 # XXX overflow...
674 sign, y, m, d, H, M, S = time
675 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
676 if seconds:
677 sign = seconds<0 and -1 or 1
678 seconds *= sign
679 S = seconds%60
680 seconds /= 60
681 M = seconds%60
682 seconds /= 60
683 H = seconds%24
684 d = seconds / 24
685 else:
686 months = y*12 + m
687 sign = months<0 and -1 or 1
688 months *= sign
689 m = months%12
690 y = months/12
692 return (sign, y, m, d, H, M, S)
694 class Range:
695 """Represents range between two values
696 Ranges can be created using one of theese two alternative syntaxes:
698 1. Native english syntax::
700 [[From] <value>][ To <value>]
702 Keywords "From" and "To" are case insensitive. Keyword "From" is
703 optional.
705 2. "Geek" syntax::
707 [<value>][; <value>]
709 Either first or second <value> can be omitted in both syntaxes.
711 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
713 >>> Range("from 2-12 to 4-2")
714 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
716 >>> Range("18:00 TO +2m")
717 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
719 >>> Range("12:00")
720 <Range from 2003-03-08.12:00:00 to None>
722 >>> Range("tO +3d")
723 <Range from None to 2003-03-11.20:07:48>
725 >>> Range("2002-11-10; 2002-12-12")
726 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
728 >>> Range("; 20:00 +1d")
729 <Range from None to 2003-03-09.20:00:00>
731 """
732 def __init__(self, spec, Type, allow_granularity=1, **params):
733 """Initializes Range of type <Type> from given <spec> string.
735 Sets two properties - from_value and to_value. None assigned to any of
736 this properties means "infinitum" (-infinitum to from_value and
737 +infinitum to to_value)
739 The Type parameter here should be class itself (e.g. Date), not a
740 class instance.
742 """
743 self.range_type = Type
744 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
745 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
746 # Check which syntax to use
747 if spec.find(';') == -1:
748 # Native english
749 mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
750 else:
751 # Geek
752 mch_range = re.search(re_geek_range, spec.strip())
753 if mch_range:
754 self.from_value, self.to_value = mch_range.groups()
755 if self.from_value:
756 self.from_value = Type(self.from_value.strip(), **params)
757 if self.to_value:
758 self.to_value = Type(self.to_value.strip(), **params)
759 else:
760 if allow_granularity:
761 self.from_value = Type(spec, **params)
762 self.to_value = Type(spec, add_granularity=1, **params)
763 else:
764 raise ValueError, "Invalid range"
766 def __str__(self):
767 return "from %s to %s" % (self.from_value, self.to_value)
769 def __repr__(self):
770 return "<Range %s>" % self.__str__()
772 def test_range():
773 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
774 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
775 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
776 for rspec in rspecs:
777 print '>>> Range("%s")' % rspec
778 print `Range(rspec, Date)`
779 print
780 for rspec in rispecs:
781 print '>>> Range("%s")' % rspec
782 print `Range(rspec, Interval)`
783 print
785 def test():
786 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
787 for interval in intervals:
788 print '>>> Interval("%s")'%interval
789 print `Interval(interval)`
791 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
792 "08-13.22:13", "14:25", '2002-12')
793 for date in dates:
794 print '>>> Date("%s")'%date
795 print `Date(date)`
797 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
798 for date, interval in sums:
799 print '>>> Date("%s") + Interval("%s")'%(date, interval)
800 print `Date(date) + Interval(interval)`
802 if __name__ == '__main__':
803 test()
805 # vim: set filetype=python ts=4 sw=4 et si