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