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.94 2007-12-23 00:23:23 richard Exp $
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
24 import calendar
25 import datetime
26 import time
27 import re
29 try:
30 import pytz
31 except ImportError:
32 pytz = None
34 from roundup import i18n
36 # no, I don't know why we must anchor the date RE when we only ever use it
37 # in a match()
38 date_re = re.compile(r'''^
39 ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
40 |(?P<a>\d\d?)[/-](?P<b>\d\d?))? # or mm-dd
41 (?P<n>\.)? # .
42 (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d?(\.\d+)?))?)? # hh:mm:ss
43 (?P<o>[\d\smywd\-+]+)? # offset
44 $''', re.VERBOSE)
45 serialised_date_re = re.compile(r'''
46 (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d?(\.\d+)?)
47 ''', re.VERBOSE)
49 _timedelta0 = datetime.timedelta(0)
51 # load UTC tzinfo
52 if pytz:
53 UTC = pytz.utc
54 else:
55 # fallback implementation from Python Library Reference
57 class _UTC(datetime.tzinfo):
59 """Universal Coordinated Time zoneinfo"""
61 def utcoffset(self, dt):
62 return _timedelta0
64 def tzname(self, dt):
65 return "UTC"
67 def dst(self, dt):
68 return _timedelta0
70 def __repr__(self):
71 return "<UTC>"
73 # pytz adjustments interface
74 # Note: pytz verifies that dt is naive datetime for localize()
75 # and not naive datetime for normalize().
76 # In this implementation, we don't care.
78 def normalize(self, dt, is_dst=False):
79 return dt.replace(tzinfo=self)
81 def localize(self, dt, is_dst=False):
82 return dt.replace(tzinfo=self)
84 UTC = _UTC()
86 # integral hours offsets were available in Roundup versions prior to 1.1.3
87 # and still are supported as a fallback if pytz module is not installed
88 class SimpleTimezone(datetime.tzinfo):
90 """Simple zoneinfo with fixed numeric offset and no daylight savings"""
92 def __init__(self, offset=0, name=None):
93 super(SimpleTimezone, self).__init__()
94 self.offset = offset
95 if name:
96 self.name = name
97 else:
98 self.name = "Etc/GMT%+d" % self.offset
100 def utcoffset(self, dt):
101 return datetime.timedelta(hours=self.offset)
103 def tzname(self, dt):
104 return self.name
106 def dst(self, dt):
107 return _timedelta0
109 def __repr__(self):
110 return "<%s: %s>" % (self.__class__.__name__, self.name)
112 # pytz adjustments interface
114 def normalize(self, dt):
115 return dt.replace(tzinfo=self)
117 def localize(self, dt, is_dst=False):
118 return dt.replace(tzinfo=self)
120 # simple timezones with fixed offset
121 _tzoffsets = dict(GMT=0, UCT=0, EST=5, MST=7, HST=10)
123 def get_timezone(tz):
124 # if tz is None, return None (will result in naive datetimes)
125 # XXX should we return UTC for None?
126 if tz is None:
127 return None
128 # try integer offset first for backward compatibility
129 try:
130 utcoffset = int(tz)
131 except (TypeError, ValueError):
132 pass
133 else:
134 if utcoffset == 0:
135 return UTC
136 else:
137 return SimpleTimezone(utcoffset)
138 # tz is a timezone name
139 if pytz:
140 return pytz.timezone(tz)
141 elif tz == "UTC":
142 return UTC
143 elif tz in _tzoffsets:
144 return SimpleTimezone(_tzoffsets[tz], tz)
145 else:
146 raise KeyError, tz
148 def _utc_to_local(y,m,d,H,M,S,tz):
149 TZ = get_timezone(tz)
150 frac = S - int(S)
151 dt = datetime.datetime(y, m, d, H, M, int(S), tzinfo=UTC)
152 y,m,d,H,M,S = dt.astimezone(TZ).timetuple()[:6]
153 S = S + frac
154 return (y,m,d,H,M,S)
156 def _local_to_utc(y,m,d,H,M,S,tz):
157 TZ = get_timezone(tz)
158 dt = datetime.datetime(y,m,d,H,M,int(S))
159 y,m,d,H,M,S = TZ.localize(dt).utctimetuple()[:6]
160 return (y,m,d,H,M,S)
162 class Date:
163 '''
164 As strings, date-and-time stamps are specified with the date in
165 international standard format (yyyy-mm-dd) joined to the time
166 (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
167 and are fairly readable when printed. An example of a valid stamp is
168 "2000-06-24.13:03:59". We'll call this the "full date format". When
169 Timestamp objects are printed as strings, they appear in the full date
170 format with the time always given in GMT. The full date format is
171 always exactly 19 characters long.
173 For user input, some partial forms are also permitted: the whole time
174 or just the seconds may be omitted; and the whole date may be omitted
175 or just the year may be omitted. If the time is given, the time is
176 interpreted in the user's local time zone. The Date constructor takes
177 care of these conversions. In the following examples, suppose that yyyy
178 is the current year, mm is the current month, and dd is the current day
179 of the month; and suppose that the user is on Eastern Standard Time.
180 Examples::
182 "2000-04-17" means <Date 2000-04-17.00:00:00>
183 "01-25" means <Date yyyy-01-25.00:00:00>
184 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
185 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
186 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
187 "14:25" means <Date yyyy-mm-dd.19:25:00>
188 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
189 "2003" means <Date 2003-01-01.00:00:00>
190 "2003-06" means <Date 2003-06-01.00:00:00>
191 "." means "right now"
193 The Date class should understand simple date expressions of the form
194 stamp + interval and stamp - interval. When adding or subtracting
195 intervals involving months or years, the components are handled
196 separately. For example, when evaluating "2000-06-25 + 1m 10d", we
197 first add one month to get 2000-07-25, then add 10 days to get
198 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
199 or 41 days). Example usage::
201 >>> Date(".")
202 <Date 2000-06-26.00:34:02>
203 >>> _.local(-5)
204 "2000-06-25.19:34:02"
205 >>> Date(". + 2d")
206 <Date 2000-06-28.00:34:02>
207 >>> Date("1997-04-17", -5)
208 <Date 1997-04-17.00:00:00>
209 >>> Date("01-25", -5)
210 <Date 2000-01-25.00:00:00>
211 >>> Date("08-13.22:13", -5)
212 <Date 2000-08-14.03:13:00>
213 >>> Date("14:25", -5)
214 <Date 2000-06-25.19:25:00>
216 The date format 'yyyymmddHHMMSS' (year, month, day, hour,
217 minute, second) is the serialisation format returned by the serialise()
218 method, and is accepted as an argument on instatiation.
220 The date class handles basic arithmetic::
222 >>> d1=Date('.')
223 >>> d1
224 <Date 2004-04-06.22:04:20.766830>
225 >>> d2=Date('2003-07-01')
226 >>> d2
227 <Date 2003-07-01.00:00:0.000000>
228 >>> d1-d2
229 <Interval + 280d 22:04:20>
230 >>> i1=_
231 >>> d2+i1
232 <Date 2004-04-06.22:04:20.000000>
233 >>> d1-i1
234 <Date 2003-07-01.00:00:0.000000>
235 '''
237 def __init__(self, spec='.', offset=0, add_granularity=False,
238 translator=i18n):
239 """Construct a date given a specification and a time zone offset.
241 'spec'
242 is a full date or a partial form, with an optional added or
243 subtracted interval. Or a date 9-tuple.
244 'offset'
245 is the local time zone offset from GMT in hours.
246 'translator'
247 is i18n module or one of gettext translation classes.
248 It must have attributes 'gettext' and 'ngettext',
249 serving as translation functions.
250 """
251 self.setTranslator(translator)
252 if type(spec) == type(''):
253 self.set(spec, offset=offset, add_granularity=add_granularity)
254 return
255 elif isinstance(spec, datetime.datetime):
256 # Python 2.3+ datetime object
257 y,m,d,H,M,S,x,x,x = spec.timetuple()
258 S += spec.microsecond/1000000.
259 spec = (y,m,d,H,M,S,x,x,x)
260 elif hasattr(spec, 'tuple'):
261 spec = spec.tuple()
262 elif isinstance(spec, Date):
263 spec = spec.get_tuple()
264 try:
265 y,m,d,H,M,S,x,x,x = spec
266 frac = S - int(S)
267 self.year, self.month, self.day, self.hour, self.minute, \
268 self.second = _local_to_utc(y, m, d, H, M, S, offset)
269 # we lost the fractional part
270 self.second = self.second + frac
271 if str(self.second) == '60.0': self.second = 59.9
272 except:
273 raise ValueError, 'Unknown spec %r' % (spec,)
275 def set(self, spec, offset=0, date_re=date_re,
276 serialised_re=serialised_date_re, add_granularity=False):
277 ''' set the date to the value in spec
278 '''
280 m = serialised_re.match(spec)
281 if m is not None:
282 # we're serialised - easy!
283 g = m.groups()
284 (self.year, self.month, self.day, self.hour, self.minute) = \
285 map(int, g[:5])
286 self.second = float(g[5])
287 return
289 # not serialised data, try usual format
290 m = date_re.match(spec)
291 if m is None:
292 raise ValueError, self._('Not a date spec: '
293 '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
294 '"yyyy-mm-dd.HH:MM:SS.SSS"')
296 info = m.groupdict()
298 # If add_granularity is true, construct the maximum time given
299 # the precision of the input. For example, given the input
300 # "12:15", construct "12:15:59". Or, for "2008", construct
301 # "2008-12-31.23:59:59".
302 if add_granularity:
303 for gran in 'SMHdmy':
304 if info[gran] is not None:
305 if gran == 'S':
306 raise ValueError
307 elif gran == 'M':
308 add_granularity = Interval('00:01')
309 elif gran == 'H':
310 add_granularity = Interval('01:00')
311 else:
312 add_granularity = Interval('+1%s'%gran)
313 break
314 else:
315 raise ValueError(self._('Could not determine granularity'))
317 # get the current date as our default
318 dt = datetime.datetime.utcnow()
319 y,m,d,H,M,S,x,x,x = dt.timetuple()
320 S += dt.microsecond/1000000.
322 # whether we need to convert to UTC
323 adjust = False
325 if info['y'] is not None or info['a'] is not None:
326 if info['y'] is not None:
327 y = int(info['y'])
328 m,d = (1,1)
329 if info['m'] is not None:
330 m = int(info['m'])
331 if info['d'] is not None:
332 d = int(info['d'])
333 if info['a'] is not None:
334 m = int(info['a'])
335 d = int(info['b'])
336 H = 0
337 M = S = 0
338 adjust = True
340 # override hour, minute, second parts
341 if info['H'] is not None and info['M'] is not None:
342 H = int(info['H'])
343 M = int(info['M'])
344 S = 0
345 if info['S'] is not None:
346 S = float(info['S'])
347 adjust = True
350 # now handle the adjustment of hour
351 frac = S - int(S)
352 dt = datetime.datetime(y,m,d,H,M,int(S), int(frac * 1000000.))
353 y, m, d, H, M, S, x, x, x = dt.timetuple()
354 if adjust:
355 y, m, d, H, M, S = _local_to_utc(y, m, d, H, M, S, offset)
356 self.year, self.month, self.day, self.hour, self.minute, \
357 self.second = y, m, d, H, M, S
358 # we lost the fractional part along the way
359 self.second += dt.microsecond/1000000.
361 if info.get('o', None):
362 try:
363 self.applyInterval(Interval(info['o'], allowdate=0))
364 except ValueError:
365 raise ValueError, self._('%r not a date / time spec '
366 '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
367 '"yyyy-mm-dd.HH:MM:SS.SSS"')%(spec,)
369 # adjust by added granularity
370 if add_granularity:
371 self.applyInterval(add_granularity)
372 self.applyInterval(Interval('- 00:00:01'))
374 def addInterval(self, interval):
375 ''' Add the interval to this date, returning the date tuple
376 '''
377 # do the basic calc
378 sign = interval.sign
379 year = self.year + sign * interval.year
380 month = self.month + sign * interval.month
381 day = self.day + sign * interval.day
382 hour = self.hour + sign * interval.hour
383 minute = self.minute + sign * interval.minute
384 # Intervals work on whole seconds
385 second = int(self.second) + sign * interval.second
387 # now cope with under- and over-flow
388 # first do the time
389 while (second < 0 or second > 59 or minute < 0 or minute > 59 or
390 hour < 0 or hour > 23):
391 if second < 0: minute -= 1; second += 60
392 elif second > 59: minute += 1; second -= 60
393 if minute < 0: hour -= 1; minute += 60
394 elif minute > 59: hour += 1; minute -= 60
395 if hour < 0: day -= 1; hour += 24
396 elif hour > 23: day += 1; hour -= 24
398 # fix up the month so we're within range
399 while month < 1 or month > 12:
400 if month < 1: year -= 1; month += 12
401 if month > 12: year += 1; month -= 12
403 # now do the days, now that we know what month we're in
404 def get_mdays(year, month):
405 if month == 2 and calendar.isleap(year): return 29
406 else: return calendar.mdays[month]
408 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
409 # now to day under/over
410 if day < 1:
411 # When going backwards, decrement month, then increment days
412 month -= 1
413 day += get_mdays(year,month)
414 elif day > get_mdays(year,month):
415 # When going forwards, decrement days, then increment month
416 day -= get_mdays(year,month)
417 month += 1
419 # possibly fix up the month so we're within range
420 while month < 1 or month > 12:
421 if month < 1: year -= 1; month += 12 ; day += 31
422 if month > 12: year += 1; month -= 12
424 return (year, month, day, hour, minute, second, 0, 0, 0)
426 def differenceDate(self, other):
427 "Return the difference between this date and another date"
428 return self - other
430 def applyInterval(self, interval):
431 ''' Apply the interval to this date
432 '''
433 self.year, self.month, self.day, self.hour, self.minute, \
434 self.second, x, x, x = self.addInterval(interval)
436 def __add__(self, interval):
437 """Add an interval to this date to produce another date.
438 """
439 return Date(self.addInterval(interval), translator=self.translator)
441 # deviates from spec to allow subtraction of dates as well
442 def __sub__(self, other):
443 """ Subtract:
444 1. an interval from this date to produce another date.
445 2. a date from this date to produce an interval.
446 """
447 if isinstance(other, Interval):
448 other = Interval(other.get_tuple())
449 other.sign *= -1
450 return self.__add__(other)
452 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
454 return self.dateDelta(other)
456 def dateDelta(self, other):
457 """ Produce an Interval of the difference between this date
458 and another date. Only returns days:hours:minutes:seconds.
459 """
460 # Returning intervals larger than a day is almost
461 # impossible - months, years, weeks, are all so imprecise.
462 a = calendar.timegm((self.year, self.month, self.day, self.hour,
463 self.minute, self.second, 0, 0, 0))
464 b = calendar.timegm((other.year, other.month, other.day,
465 other.hour, other.minute, other.second, 0, 0, 0))
466 # intervals work in whole seconds
467 diff = int(a - b)
468 if diff > 0:
469 sign = 1
470 else:
471 sign = -1
472 diff = -diff
473 S = diff%60
474 M = (diff/60)%60
475 H = (diff/(60*60))%24
476 d = diff/(24*60*60)
477 return Interval((0, 0, d, H, M, S), sign=sign,
478 translator=self.translator)
480 def __cmp__(self, other, int_seconds=0):
481 """Compare this date to another date."""
482 if other is None:
483 return 1
484 for attr in ('year', 'month', 'day', 'hour', 'minute'):
485 if not hasattr(other, attr):
486 return 1
487 r = cmp(getattr(self, attr), getattr(other, attr))
488 if r: return r
489 if not hasattr(other, 'second'):
490 return 1
491 if int_seconds:
492 return cmp(int(self.second), int(other.second))
493 return cmp(self.second, other.second)
495 def __str__(self):
496 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
497 return self.formal()
499 def formal(self, sep='.', sec='%02d'):
500 f = '%%04d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
501 return f%(self.year, self.month, self.day, self.hour, self.minute,
502 self.second)
504 def pretty(self, format='%d %B %Y'):
505 ''' print up the date date using a pretty format...
507 Note that if the day is zero, and the day appears first in the
508 format, then the day number will be removed from output.
509 '''
510 dt = datetime.datetime(self.year, self.month, self.day, self.hour,
511 self.minute, int(self.second),
512 int ((self.second - int (self.second)) * 1000000.))
513 str = dt.strftime(format)
515 # handle zero day by removing it
516 if format.startswith('%d') and str[0] == '0':
517 return ' ' + str[1:]
518 return str
520 def __repr__(self):
521 return '<Date %s>'%self.formal(sec='%06.3f')
523 def local(self, offset):
524 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
525 The offset is a pytz tz offset if pytz is installed.
526 """
527 y, m, d, H, M, S = _utc_to_local(self.year, self.month, self.day,
528 self.hour, self.minute, self.second, offset)
529 return Date((y, m, d, H, M, S, 0, 0, 0), translator=self.translator)
531 def __deepcopy__(self, memo):
532 return Date((self.year, self.month, self.day, self.hour,
533 self.minute, self.second, 0, 0, 0), translator=self.translator)
535 def get_tuple(self):
536 return (self.year, self.month, self.day, self.hour, self.minute,
537 self.second, 0, 0, 0)
539 def serialise(self):
540 return '%04d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
541 self.day, self.hour, self.minute, self.second)
543 def timestamp(self):
544 ''' return a UNIX timestamp for this date '''
545 frac = self.second - int(self.second)
546 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
547 self.minute, self.second, 0, 0, 0))
548 # we lose the fractional part
549 return ts + frac
551 def setTranslator(self, translator):
552 """Replace the translation engine
554 'translator'
555 is i18n module or one of gettext translation classes.
556 It must have attributes 'gettext' and 'ngettext',
557 serving as translation functions.
558 """
559 self.translator = translator
560 self._ = translator.gettext
561 self.ngettext = translator.ngettext
563 def fromtimestamp(cls, ts):
564 """Create a date object from a timestamp.
566 The timestamp may be outside the gmtime year-range of
567 1902-2038.
568 """
569 usec = int((ts - int(ts)) * 1000000.)
570 delta = datetime.timedelta(seconds = int(ts), microseconds = usec)
571 return cls(datetime.datetime(1970, 1, 1) + delta)
572 fromtimestamp = classmethod(fromtimestamp)
574 class Interval:
575 '''
576 Date intervals are specified using the suffixes "y", "m", and "d". The
577 suffix "w" (for "week") means 7 days. Time intervals are specified in
578 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
579 may not).
581 "3y" means three years
582 "2y 1m" means two years and one month
583 "1m 25d" means one month and 25 days
584 "2w 3d" means two weeks and three days
585 "1d 2:50" means one day, two hours, and 50 minutes
586 "14:00" means 14 hours
587 "0:04:33" means four minutes and 33 seconds
589 Example usage:
590 >>> Interval(" 3w 1 d 2:00")
591 <Interval + 22d 2:00>
592 >>> Date(". + 2d") + Interval("- 3w")
593 <Date 2000-06-07.00:34:02>
594 >>> Interval('1:59:59') + Interval('00:00:01')
595 <Interval + 2:00>
596 >>> Interval('2:00') + Interval('- 00:00:01')
597 <Interval + 1:59:59>
598 >>> Interval('1y')/2
599 <Interval + 6m>
600 >>> Interval('1:00')/2
601 <Interval + 0:30>
602 >>> Interval('2003-03-18')
603 <Interval + [number of days between now and 2003-03-18]>
604 >>> Interval('-4d 2003-03-18')
605 <Interval + [number of days between now and 2003-03-14]>
607 Interval arithmetic is handled in a couple of special ways, trying
608 to cater for the most common cases. Fundamentally, Intervals which
609 have both date and time parts will result in strange results in
610 arithmetic - because of the impossibility of handling day->month->year
611 over- and under-flows. Intervals may also be divided by some number.
613 Intervals are added to Dates in order of:
614 seconds, minutes, hours, years, months, days
616 Calculations involving months (eg '+2m') have no effect on days - only
617 days (or over/underflow from hours/mins/secs) will do that, and
618 days-per-month and leap years are accounted for. Leap seconds are not.
620 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
621 minute, second) is the serialisation format returned by the serialise()
622 method, and is accepted as an argument on instatiation.
624 TODO: more examples, showing the order of addition operation
625 '''
626 def __init__(self, spec, sign=1, allowdate=1, add_granularity=False,
627 translator=i18n
628 ):
629 """Construct an interval given a specification."""
630 self.setTranslator(translator)
631 if isinstance(spec, (int, float, long)):
632 self.from_seconds(spec)
633 elif isinstance(spec, basestring):
634 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
635 elif isinstance(spec, Interval):
636 (self.sign, self.year, self.month, self.day, self.hour,
637 self.minute, self.second) = spec.get_tuple()
638 else:
639 if len(spec) == 7:
640 self.sign, self.year, self.month, self.day, self.hour, \
641 self.minute, self.second = spec
642 self.second = int(self.second)
643 else:
644 # old, buggy spec form
645 self.sign = sign
646 self.year, self.month, self.day, self.hour, self.minute, \
647 self.second = spec
648 self.second = int(self.second)
650 def __deepcopy__(self, memo):
651 return Interval((self.sign, self.year, self.month, self.day,
652 self.hour, self.minute, self.second), translator=self.translator)
654 def set(self, spec, allowdate=1, interval_re=re.compile('''
655 \s*(?P<s>[-+])? # + or -
656 \s*((?P<y>\d+\s*)y)? # year
657 \s*((?P<m>\d+\s*)m)? # month
658 \s*((?P<w>\d+\s*)w)? # week
659 \s*((?P<d>\d+\s*)d)? # day
660 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
661 \s*(?P<D>
662 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
663 \.? # .
664 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
665 )?''', re.VERBOSE), serialised_re=re.compile('''
666 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
667 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
668 add_granularity=False):
669 ''' set the date to the value in spec
670 '''
671 self.year = self.month = self.week = self.day = self.hour = \
672 self.minute = self.second = 0
673 self.sign = 1
674 m = serialised_re.match(spec)
675 if not m:
676 m = interval_re.match(spec)
677 if not m:
678 raise ValueError, self._('Not an interval spec:'
679 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
680 else:
681 allowdate = 0
683 # pull out all the info specified
684 info = m.groupdict()
685 if add_granularity:
686 for gran in 'SMHdwmy':
687 if info[gran] is not None:
688 info[gran] = int(info[gran]) + (info['s']=='-' and -1 or 1)
689 break
691 valid = 0
692 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
693 'H':'hour', 'M':'minute', 'S':'second'}.items():
694 if info.get(group, None) is not None:
695 valid = 1
696 setattr(self, attr, int(info[group]))
698 # make sure it's valid
699 if not valid and not info['D']:
700 raise ValueError, self._('Not an interval spec:'
701 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
703 if self.week:
704 self.day = self.day + self.week*7
706 if info['s'] is not None:
707 self.sign = {'+':1, '-':-1}[info['s']]
709 # use a date spec if one is given
710 if allowdate and info['D'] is not None:
711 now = Date('.')
712 date = Date(info['D'])
713 # if no time part was specified, nuke it in the "now" date
714 if not date.hour or date.minute or date.second:
715 now.hour = now.minute = now.second = 0
716 if date != now:
717 y = now - (date + self)
718 self.__init__(y.get_tuple())
720 def __cmp__(self, other):
721 """Compare this interval to another interval."""
722 if other is None:
723 # we are always larger than None
724 return 1
725 for attr in 'sign year month day hour minute second'.split():
726 r = cmp(getattr(self, attr), getattr(other, attr))
727 if r:
728 return r
729 return 0
731 def __str__(self):
732 """Return this interval as a string."""
733 l = []
734 if self.year: l.append('%sy'%self.year)
735 if self.month: l.append('%sm'%self.month)
736 if self.day: l.append('%sd'%self.day)
737 if self.second:
738 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
739 elif self.hour or self.minute:
740 l.append('%d:%02d'%(self.hour, self.minute))
741 if l:
742 l.insert(0, {1:'+', -1:'-'}[self.sign])
743 else:
744 l.append('00:00')
745 return ' '.join(l)
747 def __add__(self, other):
748 if isinstance(other, Date):
749 # the other is a Date - produce a Date
750 return Date(other.addInterval(self), translator=self.translator)
751 elif isinstance(other, Interval):
752 # add the other Interval to this one
753 a = self.get_tuple()
754 asgn = a[0]
755 b = other.get_tuple()
756 bsgn = b[0]
757 i = [asgn*x + bsgn*y for x,y in zip(a[1:],b[1:])]
758 i.insert(0, 1)
759 i = fixTimeOverflow(i)
760 return Interval(i, translator=self.translator)
761 # nope, no idea what to do with this other...
762 raise TypeError, "Can't add %r"%other
764 def __sub__(self, other):
765 if isinstance(other, Date):
766 # the other is a Date - produce a Date
767 interval = Interval(self.get_tuple())
768 interval.sign *= -1
769 return Date(other.addInterval(interval),
770 translator=self.translator)
771 elif isinstance(other, Interval):
772 # add the other Interval to this one
773 a = self.get_tuple()
774 asgn = a[0]
775 b = other.get_tuple()
776 bsgn = b[0]
777 i = [asgn*x - bsgn*y for x,y in zip(a[1:],b[1:])]
778 i.insert(0, 1)
779 i = fixTimeOverflow(i)
780 return Interval(i, translator=self.translator)
781 # nope, no idea what to do with this other...
782 raise TypeError, "Can't add %r"%other
784 def __div__(self, other):
785 """ Divide this interval by an int value.
787 Can't divide years and months sensibly in the _same_
788 calculation as days/time, so raise an error in that situation.
789 """
790 try:
791 other = float(other)
792 except TypeError:
793 raise ValueError, "Can only divide Intervals by numbers"
795 y, m, d, H, M, S = (self.year, self.month, self.day,
796 self.hour, self.minute, self.second)
797 if y or m:
798 if d or H or M or S:
799 raise ValueError, "Can't divide Interval with date and time"
800 months = self.year*12 + self.month
801 months *= self.sign
803 months = int(months/other)
805 sign = months<0 and -1 or 1
806 m = months%12
807 y = months / 12
808 return Interval((sign, y, m, 0, 0, 0, 0),
809 translator=self.translator)
811 else:
812 # handle a day/time division
813 seconds = S + M*60 + H*60*60 + d*60*60*24
814 seconds *= self.sign
816 seconds = int(seconds/other)
818 sign = seconds<0 and -1 or 1
819 seconds *= sign
820 S = seconds%60
821 seconds /= 60
822 M = seconds%60
823 seconds /= 60
824 H = seconds%24
825 d = seconds / 24
826 return Interval((sign, 0, 0, d, H, M, S),
827 translator=self.translator)
829 def __repr__(self):
830 return '<Interval %s>'%self.__str__()
832 def pretty(self):
833 ''' print up the date date using one of these nice formats..
834 '''
835 _quarters = self.minute / 15
836 if self.year:
837 s = self.ngettext("%(number)s year", "%(number)s years",
838 self.year) % {'number': self.year}
839 elif self.month or self.day > 28:
840 _months = max(1, int(((self.month * 30) + self.day) / 30))
841 s = self.ngettext("%(number)s month", "%(number)s months",
842 _months) % {'number': _months}
843 elif self.day > 7:
844 _weeks = int(self.day / 7)
845 s = self.ngettext("%(number)s week", "%(number)s weeks",
846 _weeks) % {'number': _weeks}
847 elif self.day > 1:
848 # Note: singular form is not used
849 s = self.ngettext('%(number)s day', '%(number)s days',
850 self.day) % {'number': self.day}
851 elif self.day == 1 or self.hour > 12:
852 if self.sign > 0:
853 return self._('tomorrow')
854 else:
855 return self._('yesterday')
856 elif self.hour > 1:
857 # Note: singular form is not used
858 s = self.ngettext('%(number)s hour', '%(number)s hours',
859 self.hour) % {'number': self.hour}
860 elif self.hour == 1:
861 if self.minute < 15:
862 s = self._('an hour')
863 elif _quarters == 2:
864 s = self._('1 1/2 hours')
865 else:
866 s = self.ngettext('1 %(number)s/4 hours',
867 '1 %(number)s/4 hours', _quarters)%{'number': _quarters}
868 elif self.minute < 1:
869 if self.sign > 0:
870 return self._('in a moment')
871 else:
872 return self._('just now')
873 elif self.minute == 1:
874 # Note: used in expressions "in 1 minute" or "1 minute ago"
875 s = self._('1 minute')
876 elif self.minute < 15:
877 # Note: used in expressions "in 2 minutes" or "2 minutes ago"
878 s = self.ngettext('%(number)s minute', '%(number)s minutes',
879 self.minute) % {'number': self.minute}
880 elif _quarters == 2:
881 s = self._('1/2 an hour')
882 else:
883 s = self.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
884 _quarters) % {'number': _quarters}
885 # XXX this is internationally broken
886 if self.sign < 0:
887 s = self._('%s ago') % s
888 else:
889 s = self._('in %s') % s
890 return s
892 def get_tuple(self):
893 return (self.sign, self.year, self.month, self.day, self.hour,
894 self.minute, self.second)
896 def serialise(self):
897 sign = self.sign > 0 and '+' or '-'
898 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
899 self.day, self.hour, self.minute, self.second)
901 def as_seconds(self):
902 '''Calculate the Interval as a number of seconds.
904 Months are counted as 30 days, years as 365 days. Returns a Long
905 int.
906 '''
907 n = self.year * 365L
908 n = n + self.month * 30
909 n = n + self.day
910 n = n * 24
911 n = n + self.hour
912 n = n * 60
913 n = n + self.minute
914 n = n * 60
915 n = n + self.second
916 return n * self.sign
918 def from_seconds(self, val):
919 '''Figure my second, minute, hour and day values using a seconds
920 value.
921 '''
922 val = int(val)
923 if val < 0:
924 self.sign = -1
925 val = -val
926 else:
927 self.sign = 1
928 self.second = val % 60
929 val = val / 60
930 self.minute = val % 60
931 val = val / 60
932 self.hour = val % 24
933 val = val / 24
934 self.day = val
935 self.month = self.year = 0
937 def setTranslator(self, translator):
938 """Replace the translation engine
940 'translator'
941 is i18n module or one of gettext translation classes.
942 It must have attributes 'gettext' and 'ngettext',
943 serving as translation functions.
944 """
945 self.translator = translator
946 self._ = translator.gettext
947 self.ngettext = translator.ngettext
950 def fixTimeOverflow(time):
951 """ Handle the overflow in the time portion (H, M, S) of "time":
952 (sign, y,m,d,H,M,S)
954 Overflow and underflow will at most affect the _days_ portion of
955 the date. We do not overflow days to months as we don't know _how_
956 to, generally.
957 """
958 # XXX we could conceivably use this function for handling regular dates
959 # XXX too - we just need to interrogate the month/year for the day
960 # XXX overflow...
962 sign, y, m, d, H, M, S = time
963 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
964 if seconds:
965 sign = seconds<0 and -1 or 1
966 seconds *= sign
967 S = seconds%60
968 seconds /= 60
969 M = seconds%60
970 seconds /= 60
971 H = seconds%24
972 d = seconds / 24
973 else:
974 months = y*12 + m
975 sign = months<0 and -1 or 1
976 months *= sign
977 m = months%12
978 y = months/12
980 return (sign, y, m, d, H, M, S)
982 class Range:
983 """Represents range between two values
984 Ranges can be created using one of theese two alternative syntaxes:
986 1. Native english syntax::
988 [[From] <value>][ To <value>]
990 Keywords "From" and "To" are case insensitive. Keyword "From" is
991 optional.
993 2. "Geek" syntax::
995 [<value>][; <value>]
997 Either first or second <value> can be omitted in both syntaxes.
999 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
1001 >>> Range("from 2-12 to 4-2")
1002 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
1004 >>> Range("18:00 TO +2m")
1005 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
1007 >>> Range("12:00")
1008 <Range from 2003-03-08.12:00:00 to None>
1010 >>> Range("tO +3d")
1011 <Range from None to 2003-03-11.20:07:48>
1013 >>> Range("2002-11-10; 2002-12-12")
1014 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
1016 >>> Range("; 20:00 +1d")
1017 <Range from None to 2003-03-09.20:00:00>
1019 """
1020 def __init__(self, spec, Type, allow_granularity=True, **params):
1021 """Initializes Range of type <Type> from given <spec> string.
1023 Sets two properties - from_value and to_value. None assigned to any of
1024 this properties means "infinitum" (-infinitum to from_value and
1025 +infinitum to to_value)
1027 The Type parameter here should be class itself (e.g. Date), not a
1028 class instance.
1029 """
1030 self.range_type = Type
1031 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
1032 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
1033 # Check which syntax to use
1034 if ';' in spec:
1035 # Geek
1036 m = re.search(re_geek_range, spec.strip())
1037 else:
1038 # Native english
1039 m = re.search(re_range, spec.strip(), re.IGNORECASE)
1040 if m:
1041 self.from_value, self.to_value = m.groups()
1042 if self.from_value:
1043 self.from_value = Type(self.from_value.strip(), **params)
1044 if self.to_value:
1045 self.to_value = Type(self.to_value.strip(), **params)
1046 else:
1047 if allow_granularity:
1048 self.from_value = Type(spec, **params)
1049 self.to_value = Type(spec, add_granularity=True, **params)
1050 else:
1051 raise ValueError, "Invalid range"
1053 def __str__(self):
1054 return "from %s to %s" % (self.from_value, self.to_value)
1056 def __repr__(self):
1057 return "<Range %s>" % self.__str__()
1059 def test_range():
1060 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
1061 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
1062 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
1063 for rspec in rspecs:
1064 print '>>> Range("%s")' % rspec
1065 print `Range(rspec, Date)`
1066 print
1067 for rspec in rispecs:
1068 print '>>> Range("%s")' % rspec
1069 print `Range(rspec, Interval)`
1070 print
1072 def test():
1073 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
1074 for interval in intervals:
1075 print '>>> Interval("%s")'%interval
1076 print `Interval(interval)`
1078 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
1079 "08-13.22:13", "14:25", '2002-12')
1080 for date in dates:
1081 print '>>> Date("%s")'%date
1082 print `Date(date)`
1084 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
1085 for date, interval in sums:
1086 print '>>> Date("%s") + Interval("%s")'%(date, interval)
1087 print `Date(date) + Interval(interval)`
1089 if __name__ == '__main__':
1090 test()
1092 # vim: set filetype=python sts=4 sw=4 et si :