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.66 2004-04-13 05:28:00 richard Exp $
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
24 import time, re, calendar, types
25 from types import *
26 from i18n import _
28 def _add_granularity(src, order, value = 1):
29 '''Increment first non-None value in src dictionary ordered by 'order'
30 parameter
31 '''
32 for gran in order:
33 if src[gran]:
34 src[gran] = int(src[gran]) + value
35 break
37 class Date:
38 '''
39 As strings, date-and-time stamps are specified with the date in
40 international standard format (yyyy-mm-dd) joined to the time
41 (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
42 and are fairly readable when printed. An example of a valid stamp is
43 "2000-06-24.13:03:59". We'll call this the "full date format". When
44 Timestamp objects are printed as strings, they appear in the full date
45 format with the time always given in GMT. The full date format is
46 always exactly 19 characters long.
48 For user input, some partial forms are also permitted: the whole time
49 or just the seconds may be omitted; and the whole date may be omitted
50 or just the year may be omitted. If the time is given, the time is
51 interpreted in the user's local time zone. The Date constructor takes
52 care of these conversions. In the following examples, suppose that yyyy
53 is the current year, mm is the current month, and dd is the current day
54 of the month; and suppose that the user is on Eastern Standard Time.
55 Examples::
57 "2000-04-17" means <Date 2000-04-17.00:00:00>
58 "01-25" means <Date yyyy-01-25.00:00:00>
59 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
60 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
61 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
62 "14:25" means <Date yyyy-mm-dd.19:25:00>
63 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
64 "2003" means <Date 2003-01-01.00:00:00>
65 "2003-06" means <Date 2003-06-01.00:00:00>
66 "." means "right now"
68 The Date class should understand simple date expressions of the form
69 stamp + interval and stamp - interval. When adding or subtracting
70 intervals involving months or years, the components are handled
71 separately. For example, when evaluating "2000-06-25 + 1m 10d", we
72 first add one month to get 2000-07-25, then add 10 days to get
73 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
74 or 41 days). Example usage::
76 >>> Date(".")
77 <Date 2000-06-26.00:34:02>
78 >>> _.local(-5)
79 "2000-06-25.19:34:02"
80 >>> Date(". + 2d")
81 <Date 2000-06-28.00:34:02>
82 >>> Date("1997-04-17", -5)
83 <Date 1997-04-17.00:00:00>
84 >>> Date("01-25", -5)
85 <Date 2000-01-25.00:00:00>
86 >>> Date("08-13.22:13", -5)
87 <Date 2000-08-14.03:13:00>
88 >>> Date("14:25", -5)
89 <Date 2000-06-25.19:25:00>
91 The date format 'yyyymmddHHMMSS' (year, month, day, hour,
92 minute, second) is the serialisation format returned by the serialise()
93 method, and is accepted as an argument on instatiation.
95 The date class handles basic arithmetic::
97 >>> d1=Date('.')
98 >>> d1
99 <Date 2004-04-06.22:04:20.766830>
100 >>> d2=Date('2003-07-01')
101 >>> d2
102 <Date 2003-07-01.00:00:0.000000>
103 >>> d1-d2
104 <Interval + 280d 22:04:20>
105 >>> i1=_
106 >>> d2+i1
107 <Date 2004-04-06.22:04:20.000000>
108 >>> d1-i1
109 <Date 2003-07-01.00:00:0.000000>
110 '''
112 def __init__(self, spec='.', offset=0, add_granularity=0):
113 """Construct a date given a specification and a time zone offset.
115 'spec'
116 is a full date or a partial form, with an optional added or
117 subtracted interval. Or a date 9-tuple.
118 'offset'
119 is the local time zone offset from GMT in hours.
120 """
121 if type(spec) == type(''):
122 self.set(spec, offset=offset, add_granularity=add_granularity)
123 return
124 elif hasattr(spec, 'tuple'):
125 spec = spec.tuple()
126 try:
127 y,m,d,H,M,S,x,x,x = spec
128 frac = S - int(S)
129 ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
130 self.year, self.month, self.day, self.hour, self.minute, \
131 self.second, x, x, x = time.gmtime(ts)
132 # we lost the fractional part
133 self.second = self.second + frac
134 except:
135 raise ValueError, 'Unknown spec %r'%spec
137 usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS.SSS][offset]'
138 def set(self, spec, offset=0, date_re=re.compile(r'''
139 ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
140 |(?P<a>\d\d?)[/-](?P<b>\d\d?))? # or mm-dd
141 (?P<n>\.)? # .
142 (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d(\.\d+)?))?)? # hh:mm:ss
143 (?P<o>.+)? # offset
144 ''', re.VERBOSE), serialised_re=re.compile(r'''
145 (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d(\.\d+)?)
146 ''', re.VERBOSE), add_granularity=0):
147 ''' set the date to the value in spec
148 '''
150 m = serialised_re.match(spec)
151 if m is not None:
152 # we're serialised - easy!
153 g = m.groups()
154 (self.year, self.month, self.day, self.hour, self.minute) = \
155 map(int, g[:5])
156 self.second = float(g[5])
157 return
159 # not serialised data, try usual format
160 m = date_re.match(spec)
161 if m is None:
162 raise ValueError, _('Not a date spec: %s' % self.usagespec)
164 info = m.groupdict()
166 if add_granularity:
167 _add_granularity(info, 'SMHdmyab')
169 # get the current date as our default
170 ts = time.time()
171 frac = ts - int(ts)
172 y,m,d,H,M,S,x,x,x = time.gmtime(ts)
173 # gmtime loses the fractional seconds
174 S = S + frac
176 if info['y'] is not None or info['a'] is not None:
177 if info['y'] is not None:
178 y = int(info['y'])
179 m,d = (1,1)
180 if info['m'] is not None:
181 m = int(info['m'])
182 if info['d'] is not None:
183 d = int(info['d'])
184 if info['a'] is not None:
185 m = int(info['a'])
186 d = int(info['b'])
187 H = -offset
188 M = S = 0
190 # override hour, minute, second parts
191 if info['H'] is not None and info['M'] is not None:
192 H = int(info['H']) - offset
193 M = int(info['M'])
194 S = 0
195 if info['S'] is not None:
196 S = float(info['S'])
198 if add_granularity:
199 S = S - 1
201 # now handle the adjustment of hour
202 frac = S - int(S)
203 ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
204 self.year, self.month, self.day, self.hour, self.minute, \
205 self.second, x, x, x = time.gmtime(ts)
206 # we lost the fractional part along the way
207 self.second = self.second + frac
209 if info.get('o', None):
210 try:
211 self.applyInterval(Interval(info['o'], allowdate=0))
212 except ValueError:
213 raise ValueError, _('%r not a date spec (%s)')%(spec,
214 self.usagespec)
216 def addInterval(self, interval):
217 ''' Add the interval to this date, returning the date tuple
218 '''
219 # do the basic calc
220 sign = interval.sign
221 year = self.year + sign * interval.year
222 month = self.month + sign * interval.month
223 day = self.day + sign * interval.day
224 hour = self.hour + sign * interval.hour
225 minute = self.minute + sign * interval.minute
226 # Intervals work on whole seconds
227 second = int(self.second) + sign * interval.second
229 # now cope with under- and over-flow
230 # first do the time
231 while (second < 0 or second > 59 or minute < 0 or minute > 59 or
232 hour < 0 or hour > 23):
233 if second < 0: minute -= 1; second += 60
234 elif second > 59: minute += 1; second -= 60
235 if minute < 0: hour -= 1; minute += 60
236 elif minute > 59: hour += 1; minute -= 60
237 if hour < 0: day -= 1; hour += 24
238 elif hour > 23: day += 1; hour -= 24
240 # fix up the month so we're within range
241 while month < 1 or month > 12:
242 if month < 1: year -= 1; month += 12
243 if month > 12: year += 1; month -= 12
245 # now do the days, now that we know what month we're in
246 def get_mdays(year, month):
247 if month == 2 and calendar.isleap(year): return 29
248 else: return calendar.mdays[month]
250 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
251 # now to day under/over
252 if day < 1:
253 # When going backwards, decrement month, then increment days
254 month -= 1
255 day += get_mdays(year,month)
256 elif day > get_mdays(year,month):
257 # When going forwards, decrement days, then increment month
258 day -= get_mdays(year,month)
259 month += 1
261 # possibly fix up the month so we're within range
262 while month < 1 or month > 12:
263 if month < 1: year -= 1; month += 12 ; day += 31
264 if month > 12: year += 1; month -= 12
266 return (year, month, day, hour, minute, second, 0, 0, 0)
268 def differenceDate(self, other):
269 "Return the difference between this date and another date"
270 return self - other
272 def applyInterval(self, interval):
273 ''' Apply the interval to this date
274 '''
275 self.year, self.month, self.day, self.hour, self.minute, \
276 self.second, x, x, x = self.addInterval(interval)
278 def __add__(self, interval):
279 """Add an interval to this date to produce another date.
280 """
281 return Date(self.addInterval(interval))
283 # deviates from spec to allow subtraction of dates as well
284 def __sub__(self, other):
285 """ Subtract:
286 1. an interval from this date to produce another date.
287 2. a date from this date to produce an interval.
288 """
289 if isinstance(other, Interval):
290 other = Interval(other.get_tuple())
291 other.sign *= -1
292 return self.__add__(other)
294 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
296 return self.dateDelta(other)
298 def dateDelta(self, other):
299 """ Produce an Interval of the difference between this date
300 and another date. Only returns days:hours:minutes:seconds.
301 """
302 # Returning intervals larger than a day is almost
303 # impossible - months, years, weeks, are all so imprecise.
304 a = calendar.timegm((self.year, self.month, self.day, self.hour,
305 self.minute, self.second, 0, 0, 0))
306 b = calendar.timegm((other.year, other.month, other.day,
307 other.hour, other.minute, other.second, 0, 0, 0))
308 # intervals work in whole seconds
309 diff = int(a - b)
310 if diff > 0:
311 sign = 1
312 else:
313 sign = -1
314 diff = -diff
315 S = diff%60
316 M = (diff/60)%60
317 H = (diff/(60*60))%24
318 d = diff/(24*60*60)
319 return Interval((0, 0, d, H, M, S), sign=sign)
321 def __cmp__(self, other, int_seconds=0):
322 """Compare this date to another date."""
323 if other is None:
324 return 1
325 for attr in ('year', 'month', 'day', 'hour', 'minute'):
326 if not hasattr(other, attr):
327 return 1
328 r = cmp(getattr(self, attr), getattr(other, attr))
329 if r: return r
330 if not hasattr(other, 'second'):
331 return 1
332 if int_seconds:
333 return cmp(int(self.second), int(other.second))
334 return cmp(self.second, other.second)
336 def __str__(self):
337 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
338 return self.formal()
340 def formal(self, sep='.', sec='%02d'):
341 f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
342 return f%(self.year, self.month, self.day, self.hour, self.minute,
343 self.second)
345 def pretty(self, format='%d %B %Y'):
346 ''' print up the date date using a pretty format...
348 Note that if the day is zero, and the day appears first in the
349 format, then the day number will be removed from output.
350 '''
351 str = time.strftime(format, (self.year, self.month, self.day,
352 self.hour, self.minute, self.second, 0, 0, 0))
353 # handle zero day by removing it
354 if format.startswith('%d') and str[0] == '0':
355 return ' ' + str[1:]
356 return str
358 def __repr__(self):
359 return '<Date %s>'%self.formal(sec='%f')
361 def local(self, offset):
362 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
363 """
364 return Date((self.year, self.month, self.day, self.hour + offset,
365 self.minute, self.second, 0, 0, 0))
367 def get_tuple(self):
368 return (self.year, self.month, self.day, self.hour, self.minute,
369 self.second, 0, 0, 0)
371 def serialise(self):
372 return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
373 self.day, self.hour, self.minute, self.second)
375 def timestamp(self):
376 ''' return a UNIX timestamp for this date '''
377 frac = self.second - int(self.second)
378 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
379 self.minute, self.second, 0, 0, 0))
380 # we lose the fractional part
381 return ts + frac
383 class Interval:
384 '''
385 Date intervals are specified using the suffixes "y", "m", and "d". The
386 suffix "w" (for "week") means 7 days. Time intervals are specified in
387 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
388 may not).
390 "3y" means three years
391 "2y 1m" means two years and one month
392 "1m 25d" means one month and 25 days
393 "2w 3d" means two weeks and three days
394 "1d 2:50" means one day, two hours, and 50 minutes
395 "14:00" means 14 hours
396 "0:04:33" means four minutes and 33 seconds
398 Example usage:
399 >>> Interval(" 3w 1 d 2:00")
400 <Interval + 22d 2:00>
401 >>> Date(". + 2d") + Interval("- 3w")
402 <Date 2000-06-07.00:34:02>
403 >>> Interval('1:59:59') + Interval('00:00:01')
404 <Interval + 2:00>
405 >>> Interval('2:00') + Interval('- 00:00:01')
406 <Interval + 1:59:59>
407 >>> Interval('1y')/2
408 <Interval + 6m>
409 >>> Interval('1:00')/2
410 <Interval + 0:30>
411 >>> Interval('2003-03-18')
412 <Interval + [number of days between now and 2003-03-18]>
413 >>> Interval('-4d 2003-03-18')
414 <Interval + [number of days between now and 2003-03-14]>
416 Interval arithmetic is handled in a couple of special ways, trying
417 to cater for the most common cases. Fundamentally, Intervals which
418 have both date and time parts will result in strange results in
419 arithmetic - because of the impossibility of handling day->month->year
420 over- and under-flows. Intervals may also be divided by some number.
422 Intervals are added to Dates in order of:
423 seconds, minutes, hours, years, months, days
425 Calculations involving months (eg '+2m') have no effect on days - only
426 days (or over/underflow from hours/mins/secs) will do that, and
427 days-per-month and leap years are accounted for. Leap seconds are not.
429 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
430 minute, second) is the serialisation format returned by the serialise()
431 method, and is accepted as an argument on instatiation.
433 TODO: more examples, showing the order of addition operation
434 '''
435 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
436 """Construct an interval given a specification."""
437 if type(spec) in (IntType, FloatType, LongType):
438 self.from_seconds(spec)
439 elif type(spec) in (StringType, UnicodeType):
440 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
441 else:
442 if len(spec) == 7:
443 self.sign, self.year, self.month, self.day, self.hour, \
444 self.minute, self.second = spec
445 self.second = int(self.second)
446 else:
447 # old, buggy spec form
448 self.sign = sign
449 self.year, self.month, self.day, self.hour, self.minute, \
450 self.second = spec
451 self.second = int(self.second)
453 def set(self, spec, allowdate=1, interval_re=re.compile('''
454 \s*(?P<s>[-+])? # + or -
455 \s*((?P<y>\d+\s*)y)? # year
456 \s*((?P<m>\d+\s*)m)? # month
457 \s*((?P<w>\d+\s*)w)? # week
458 \s*((?P<d>\d+\s*)d)? # day
459 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
460 \s*(?P<D>
461 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
462 \.? # .
463 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
464 )?''', re.VERBOSE), serialised_re=re.compile('''
465 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
466 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
467 add_granularity=0):
468 ''' set the date to the value in spec
469 '''
470 self.year = self.month = self.week = self.day = self.hour = \
471 self.minute = self.second = 0
472 self.sign = 1
473 m = serialised_re.match(spec)
474 if not m:
475 m = interval_re.match(spec)
476 if not m:
477 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
478 '[#d] [[[H]H:MM]:SS] [date spec]')
479 else:
480 allowdate = 0
482 # pull out all the info specified
483 info = m.groupdict()
484 if add_granularity:
485 _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
487 valid = 0
488 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
489 'H':'hour', 'M':'minute', 'S':'second'}.items():
490 if info.get(group, None) is not None:
491 valid = 1
492 setattr(self, attr, int(info[group]))
494 # make sure it's valid
495 if not valid and not info['D']:
496 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
497 '[#d] [[[H]H:MM]:SS]')
499 if self.week:
500 self.day = self.day + self.week*7
502 if info['s'] is not None:
503 self.sign = {'+':1, '-':-1}[info['s']]
505 # use a date spec if one is given
506 if allowdate and info['D'] is not None:
507 now = Date('.')
508 date = Date(info['D'])
509 # if no time part was specified, nuke it in the "now" date
510 if not date.hour or date.minute or date.second:
511 now.hour = now.minute = now.second = 0
512 if date != now:
513 y = now - (date + self)
514 self.__init__(y.get_tuple())
516 def __cmp__(self, other):
517 """Compare this interval to another interval."""
518 if other is None:
519 # we are always larger than None
520 return 1
521 for attr in 'sign year month day hour minute second'.split():
522 r = cmp(getattr(self, attr), getattr(other, attr))
523 if r:
524 return r
525 return 0
527 def __str__(self):
528 """Return this interval as a string."""
529 l = []
530 if self.year: l.append('%sy'%self.year)
531 if self.month: l.append('%sm'%self.month)
532 if self.day: l.append('%sd'%self.day)
533 if self.second:
534 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
535 elif self.hour or self.minute:
536 l.append('%d:%02d'%(self.hour, self.minute))
537 if l:
538 l.insert(0, {1:'+', -1:'-'}[self.sign])
539 else:
540 l.append('00:00')
541 return ' '.join(l)
543 def __add__(self, other):
544 if isinstance(other, Date):
545 # the other is a Date - produce a Date
546 return Date(other.addInterval(self))
547 elif isinstance(other, Interval):
548 # add the other Interval to this one
549 a = self.get_tuple()
550 as = a[0]
551 b = other.get_tuple()
552 bs = b[0]
553 i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
554 i.insert(0, 1)
555 i = fixTimeOverflow(i)
556 return Interval(i)
557 # nope, no idea what to do with this other...
558 raise TypeError, "Can't add %r"%other
560 def __sub__(self, other):
561 if isinstance(other, Date):
562 # the other is a Date - produce a Date
563 interval = Interval(self.get_tuple())
564 interval.sign *= -1
565 return Date(other.addInterval(interval))
566 elif isinstance(other, Interval):
567 # add the other Interval to this one
568 a = self.get_tuple()
569 as = a[0]
570 b = other.get_tuple()
571 bs = b[0]
572 i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
573 i.insert(0, 1)
574 i = fixTimeOverflow(i)
575 return Interval(i)
576 # nope, no idea what to do with this other...
577 raise TypeError, "Can't add %r"%other
579 def __div__(self, other):
580 """ Divide this interval by an int value.
582 Can't divide years and months sensibly in the _same_
583 calculation as days/time, so raise an error in that situation.
584 """
585 try:
586 other = float(other)
587 except TypeError:
588 raise ValueError, "Can only divide Intervals by numbers"
590 y, m, d, H, M, S = (self.year, self.month, self.day,
591 self.hour, self.minute, self.second)
592 if y or m:
593 if d or H or M or S:
594 raise ValueError, "Can't divide Interval with date and time"
595 months = self.year*12 + self.month
596 months *= self.sign
598 months = int(months/other)
600 sign = months<0 and -1 or 1
601 m = months%12
602 y = months / 12
603 return Interval((sign, y, m, 0, 0, 0, 0))
605 else:
606 # handle a day/time division
607 seconds = S + M*60 + H*60*60 + d*60*60*24
608 seconds *= self.sign
610 seconds = int(seconds/other)
612 sign = seconds<0 and -1 or 1
613 seconds *= sign
614 S = seconds%60
615 seconds /= 60
616 M = seconds%60
617 seconds /= 60
618 H = seconds%24
619 d = seconds / 24
620 return Interval((sign, 0, 0, d, H, M, S))
622 def __repr__(self):
623 return '<Interval %s>'%self.__str__()
625 def pretty(self):
626 ''' print up the date date using one of these nice formats..
627 '''
628 if self.year:
629 if self.year == 1:
630 s = _('1 year')
631 else:
632 s = _('%(number)s years')%{'number': self.year}
633 elif self.month or self.day > 13:
634 days = (self.month * 30) + self.day
635 if days > 28:
636 if int(days/30) > 1:
637 s = _('%(number)s months')%{'number': int(days/30)}
638 else:
639 s = _('1 month')
640 else:
641 s = _('%(number)s weeks')%{'number': int(days/7)}
642 elif self.day > 7:
643 s = _('1 week')
644 elif self.day > 1:
645 s = _('%(number)s days')%{'number': self.day}
646 elif self.day == 1 or self.hour > 12:
647 if self.sign > 0:
648 return _('tomorrow')
649 else:
650 return _('yesterday')
651 elif self.hour > 1:
652 s = _('%(number)s hours')%{'number': self.hour}
653 elif self.hour == 1:
654 if self.minute < 15:
655 s = _('an hour')
656 elif self.minute/15 == 2:
657 s = _('1 1/2 hours')
658 else:
659 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
660 elif self.minute < 1:
661 if self.sign > 0:
662 return _('in a moment')
663 else:
664 return _('just now')
665 elif self.minute == 1:
666 s = _('1 minute')
667 elif self.minute < 15:
668 s = _('%(number)s minutes')%{'number': self.minute}
669 elif int(self.minute/15) == 2:
670 s = _('1/2 an hour')
671 else:
672 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
673 if self.sign < 0:
674 s = s + _(' ago')
675 else:
676 s = _('in ') + s
677 return s
679 def get_tuple(self):
680 return (self.sign, self.year, self.month, self.day, self.hour,
681 self.minute, self.second)
683 def serialise(self):
684 sign = self.sign > 0 and '+' or '-'
685 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
686 self.day, self.hour, self.minute, self.second)
688 def as_seconds(self):
689 '''Calculate the Interval as a number of seconds.
691 Months are counted as 30 days, years as 365 days. Returns a Long
692 int.
693 '''
694 n = self.year * 365L
695 n = n + self.month * 30
696 n = n + self.day
697 n = n * 24
698 n = n + self.hour
699 n = n * 60
700 n = n + self.minute
701 n = n * 60
702 n = n + self.second
703 return n * self.sign
705 def from_seconds(self, val):
706 '''Figure my second, minute, hour and day values using a seconds
707 value.
708 '''
709 if val < 0:
710 self.sign = -1
711 val = -val
712 else:
713 self.sign = 1
714 self.second = val % 60
715 val = val / 60
716 self.minute = val % 60
717 val = val / 60
718 self.hour = val % 24
719 val = val / 24
720 self.day = val
721 self.month = self.year = 0
724 def fixTimeOverflow(time):
725 """ Handle the overflow in the time portion (H, M, S) of "time":
726 (sign, y,m,d,H,M,S)
728 Overflow and underflow will at most affect the _days_ portion of
729 the date. We do not overflow days to months as we don't know _how_
730 to, generally.
731 """
732 # XXX we could conceivably use this function for handling regular dates
733 # XXX too - we just need to interrogate the month/year for the day
734 # XXX overflow...
736 sign, y, m, d, H, M, S = time
737 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
738 if seconds:
739 sign = seconds<0 and -1 or 1
740 seconds *= sign
741 S = seconds%60
742 seconds /= 60
743 M = seconds%60
744 seconds /= 60
745 H = seconds%24
746 d = seconds / 24
747 else:
748 months = y*12 + m
749 sign = months<0 and -1 or 1
750 months *= sign
751 m = months%12
752 y = months/12
754 return (sign, y, m, d, H, M, S)
756 class Range:
757 """Represents range between two values
758 Ranges can be created using one of theese two alternative syntaxes:
760 1. Native english syntax::
762 [[From] <value>][ To <value>]
764 Keywords "From" and "To" are case insensitive. Keyword "From" is
765 optional.
767 2. "Geek" syntax::
769 [<value>][; <value>]
771 Either first or second <value> can be omitted in both syntaxes.
773 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
775 >>> Range("from 2-12 to 4-2")
776 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
778 >>> Range("18:00 TO +2m")
779 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
781 >>> Range("12:00")
782 <Range from 2003-03-08.12:00:00 to None>
784 >>> Range("tO +3d")
785 <Range from None to 2003-03-11.20:07:48>
787 >>> Range("2002-11-10; 2002-12-12")
788 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
790 >>> Range("; 20:00 +1d")
791 <Range from None to 2003-03-09.20:00:00>
793 """
794 def __init__(self, spec, Type, allow_granularity=1, **params):
795 """Initializes Range of type <Type> from given <spec> string.
797 Sets two properties - from_value and to_value. None assigned to any of
798 this properties means "infinitum" (-infinitum to from_value and
799 +infinitum to to_value)
801 The Type parameter here should be class itself (e.g. Date), not a
802 class instance.
804 """
805 self.range_type = Type
806 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
807 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
808 # Check which syntax to use
809 if spec.find(';') == -1:
810 # Native english
811 mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
812 else:
813 # Geek
814 mch_range = re.search(re_geek_range, spec.strip())
815 if mch_range:
816 self.from_value, self.to_value = mch_range.groups()
817 if self.from_value:
818 self.from_value = Type(self.from_value.strip(), **params)
819 if self.to_value:
820 self.to_value = Type(self.to_value.strip(), **params)
821 else:
822 if allow_granularity:
823 self.from_value = Type(spec, **params)
824 self.to_value = Type(spec, add_granularity=1, **params)
825 else:
826 raise ValueError, "Invalid range"
828 def __str__(self):
829 return "from %s to %s" % (self.from_value, self.to_value)
831 def __repr__(self):
832 return "<Range %s>" % self.__str__()
834 def test_range():
835 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
836 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
837 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
838 for rspec in rspecs:
839 print '>>> Range("%s")' % rspec
840 print `Range(rspec, Date)`
841 print
842 for rspec in rispecs:
843 print '>>> Range("%s")' % rspec
844 print `Range(rspec, Interval)`
845 print
847 def test():
848 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
849 for interval in intervals:
850 print '>>> Interval("%s")'%interval
851 print `Interval(interval)`
853 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
854 "08-13.22:13", "14:25", '2002-12')
855 for date in dates:
856 print '>>> Date("%s")'%date
857 print `Date(date)`
859 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
860 for date, interval in sums:
861 print '>>> Date("%s") + Interval("%s")'%(date, interval)
862 print `Date(date) + Interval(interval)`
864 if __name__ == '__main__':
865 test()
867 # vim: set filetype=python ts=4 sw=4 et si