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