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 # Python 2.3+ datetime object
253 # common case when reading from database: avoid double-conversion
254 if isinstance(spec, datetime.datetime):
255 if offset == 0:
256 self.year, self.month, self.day, self.hour, self.minute, \
257 self.second = spec.timetuple()[:6]
258 else:
259 TZ = get_timezone(tz)
260 self.year, self.month, self.day, self.hour, self.minute, \
261 self.second = TZ.localize(spec).utctimetuple()[:6]
262 self.second += spec.microsecond/1000000.
263 return
265 if type(spec) == type(''):
266 self.set(spec, offset=offset, add_granularity=add_granularity)
267 return
268 elif hasattr(spec, 'tuple'):
269 spec = spec.tuple()
270 elif isinstance(spec, Date):
271 spec = spec.get_tuple()
272 try:
273 y,m,d,H,M,S,x,x,x = spec
274 frac = S - int(S)
275 self.year, self.month, self.day, self.hour, self.minute, \
276 self.second = _local_to_utc(y, m, d, H, M, S, offset)
277 # we lost the fractional part
278 self.second = self.second + frac
279 if str(self.second) == '60.0': self.second = 59.9
280 except:
281 raise ValueError, 'Unknown spec %r' % (spec,)
283 def set(self, spec, offset=0, date_re=date_re,
284 serialised_re=serialised_date_re, add_granularity=False):
285 ''' set the date to the value in spec
286 '''
288 m = serialised_re.match(spec)
289 if m is not None:
290 # we're serialised - easy!
291 g = m.groups()
292 (self.year, self.month, self.day, self.hour, self.minute) = \
293 map(int, g[:5])
294 self.second = float(g[5])
295 return
297 # not serialised data, try usual format
298 m = date_re.match(spec)
299 if m is None:
300 raise ValueError, self._('Not a date spec: '
301 '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
302 '"yyyy-mm-dd.HH:MM:SS.SSS"')
304 info = m.groupdict()
306 # If add_granularity is true, construct the maximum time given
307 # the precision of the input. For example, given the input
308 # "12:15", construct "12:15:59". Or, for "2008", construct
309 # "2008-12-31.23:59:59".
310 if add_granularity:
311 for gran in 'SMHdmy':
312 if info[gran] is not None:
313 if gran == 'S':
314 raise ValueError
315 elif gran == 'M':
316 add_granularity = Interval('00:01')
317 elif gran == 'H':
318 add_granularity = Interval('01:00')
319 else:
320 add_granularity = Interval('+1%s'%gran)
321 break
322 else:
323 raise ValueError(self._('Could not determine granularity'))
325 # get the current date as our default
326 dt = datetime.datetime.utcnow()
327 y,m,d,H,M,S,x,x,x = dt.timetuple()
328 S += dt.microsecond/1000000.
330 # whether we need to convert to UTC
331 adjust = False
333 if info['y'] is not None or info['a'] is not None:
334 if info['y'] is not None:
335 y = int(info['y'])
336 m,d = (1,1)
337 if info['m'] is not None:
338 m = int(info['m'])
339 if info['d'] is not None:
340 d = int(info['d'])
341 if info['a'] is not None:
342 m = int(info['a'])
343 d = int(info['b'])
344 H = 0
345 M = S = 0
346 adjust = True
348 # override hour, minute, second parts
349 if info['H'] is not None and info['M'] is not None:
350 H = int(info['H'])
351 M = int(info['M'])
352 S = 0
353 if info['S'] is not None:
354 S = float(info['S'])
355 adjust = True
358 # now handle the adjustment of hour
359 frac = S - int(S)
360 dt = datetime.datetime(y,m,d,H,M,int(S), int(frac * 1000000.))
361 y, m, d, H, M, S, x, x, x = dt.timetuple()
362 if adjust:
363 y, m, d, H, M, S = _local_to_utc(y, m, d, H, M, S, offset)
364 self.year, self.month, self.day, self.hour, self.minute, \
365 self.second = y, m, d, H, M, S
366 # we lost the fractional part along the way
367 self.second += dt.microsecond/1000000.
369 if info.get('o', None):
370 try:
371 self.applyInterval(Interval(info['o'], allowdate=0))
372 except ValueError:
373 raise ValueError, self._('%r not a date / time spec '
374 '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
375 '"yyyy-mm-dd.HH:MM:SS.SSS"')%(spec,)
377 # adjust by added granularity
378 if add_granularity:
379 self.applyInterval(add_granularity)
380 self.applyInterval(Interval('- 00:00:01'))
382 def addInterval(self, interval):
383 ''' Add the interval to this date, returning the date tuple
384 '''
385 # do the basic calc
386 sign = interval.sign
387 year = self.year + sign * interval.year
388 month = self.month + sign * interval.month
389 day = self.day + sign * interval.day
390 hour = self.hour + sign * interval.hour
391 minute = self.minute + sign * interval.minute
392 # Intervals work on whole seconds
393 second = int(self.second) + sign * interval.second
395 # now cope with under- and over-flow
396 # first do the time
397 while (second < 0 or second > 59 or minute < 0 or minute > 59 or
398 hour < 0 or hour > 23):
399 if second < 0: minute -= 1; second += 60
400 elif second > 59: minute += 1; second -= 60
401 if minute < 0: hour -= 1; minute += 60
402 elif minute > 59: hour += 1; minute -= 60
403 if hour < 0: day -= 1; hour += 24
404 elif hour > 23: day += 1; hour -= 24
406 # fix up the month so we're within range
407 while month < 1 or month > 12:
408 if month < 1: year -= 1; month += 12
409 if month > 12: year += 1; month -= 12
411 # now do the days, now that we know what month we're in
412 def get_mdays(year, month):
413 if month == 2 and calendar.isleap(year): return 29
414 else: return calendar.mdays[month]
416 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
417 # now to day under/over
418 if day < 1:
419 # When going backwards, decrement month, then increment days
420 month -= 1
421 day += get_mdays(year,month)
422 elif day > get_mdays(year,month):
423 # When going forwards, decrement days, then increment month
424 day -= get_mdays(year,month)
425 month += 1
427 # possibly fix up the month so we're within range
428 while month < 1 or month > 12:
429 if month < 1: year -= 1; month += 12 ; day += 31
430 if month > 12: year += 1; month -= 12
432 return (year, month, day, hour, minute, second, 0, 0, 0)
434 def differenceDate(self, other):
435 "Return the difference between this date and another date"
436 return self - other
438 def applyInterval(self, interval):
439 ''' Apply the interval to this date
440 '''
441 self.year, self.month, self.day, self.hour, self.minute, \
442 self.second, x, x, x = self.addInterval(interval)
444 def __add__(self, interval):
445 """Add an interval to this date to produce another date.
446 """
447 return Date(self.addInterval(interval), translator=self.translator)
449 # deviates from spec to allow subtraction of dates as well
450 def __sub__(self, other):
451 """ Subtract:
452 1. an interval from this date to produce another date.
453 2. a date from this date to produce an interval.
454 """
455 if isinstance(other, Interval):
456 other = Interval(other.get_tuple())
457 other.sign *= -1
458 return self.__add__(other)
460 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
462 return self.dateDelta(other)
464 def dateDelta(self, other):
465 """ Produce an Interval of the difference between this date
466 and another date. Only returns days:hours:minutes:seconds.
467 """
468 # Returning intervals larger than a day is almost
469 # impossible - months, years, weeks, are all so imprecise.
470 a = calendar.timegm((self.year, self.month, self.day, self.hour,
471 self.minute, self.second, 0, 0, 0))
472 b = calendar.timegm((other.year, other.month, other.day,
473 other.hour, other.minute, other.second, 0, 0, 0))
474 # intervals work in whole seconds
475 diff = int(a - b)
476 if diff > 0:
477 sign = 1
478 else:
479 sign = -1
480 diff = -diff
481 S = diff%60
482 M = (diff/60)%60
483 H = (diff/(60*60))%24
484 d = diff/(24*60*60)
485 return Interval((0, 0, d, H, M, S), sign=sign,
486 translator=self.translator)
488 def __cmp__(self, other, int_seconds=0):
489 """Compare this date to another date."""
490 if other is None:
491 return 1
492 for attr in ('year', 'month', 'day', 'hour', 'minute'):
493 if not hasattr(other, attr):
494 return 1
495 r = cmp(getattr(self, attr), getattr(other, attr))
496 if r: return r
497 if not hasattr(other, 'second'):
498 return 1
499 if int_seconds:
500 return cmp(int(self.second), int(other.second))
501 return cmp(self.second, other.second)
503 def __str__(self):
504 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
505 return self.formal()
507 def formal(self, sep='.', sec='%02d'):
508 f = '%%04d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
509 return f%(self.year, self.month, self.day, self.hour, self.minute,
510 self.second)
512 def pretty(self, format='%d %B %Y'):
513 ''' print up the date date using a pretty format...
515 Note that if the day is zero, and the day appears first in the
516 format, then the day number will be removed from output.
517 '''
518 dt = datetime.datetime(self.year, self.month, self.day, self.hour,
519 self.minute, int(self.second),
520 int ((self.second - int (self.second)) * 1000000.))
521 str = dt.strftime(format)
523 # handle zero day by removing it
524 if format.startswith('%d') and str[0] == '0':
525 return ' ' + str[1:]
526 return str
528 def __repr__(self):
529 return '<Date %s>'%self.formal(sec='%06.3f')
531 def local(self, offset):
532 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
533 The offset is a pytz tz offset if pytz is installed.
534 """
535 y, m, d, H, M, S = _utc_to_local(self.year, self.month, self.day,
536 self.hour, self.minute, self.second, offset)
537 return Date((y, m, d, H, M, S, 0, 0, 0), translator=self.translator)
539 def __deepcopy__(self, memo):
540 return Date((self.year, self.month, self.day, self.hour,
541 self.minute, self.second, 0, 0, 0), translator=self.translator)
543 def get_tuple(self):
544 return (self.year, self.month, self.day, self.hour, self.minute,
545 self.second, 0, 0, 0)
547 def serialise(self):
548 return '%04d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
549 self.day, self.hour, self.minute, self.second)
551 def timestamp(self):
552 ''' return a UNIX timestamp for this date '''
553 frac = self.second - int(self.second)
554 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
555 self.minute, self.second, 0, 0, 0))
556 # we lose the fractional part
557 return ts + frac
559 def setTranslator(self, translator):
560 """Replace the translation engine
562 'translator'
563 is i18n module or one of gettext translation classes.
564 It must have attributes 'gettext' and 'ngettext',
565 serving as translation functions.
566 """
567 self.translator = translator
568 self._ = translator.gettext
569 self.ngettext = translator.ngettext
571 def fromtimestamp(cls, ts):
572 """Create a date object from a timestamp.
574 The timestamp may be outside the gmtime year-range of
575 1902-2038.
576 """
577 usec = int((ts - int(ts)) * 1000000.)
578 delta = datetime.timedelta(seconds = int(ts), microseconds = usec)
579 return cls(datetime.datetime(1970, 1, 1) + delta)
580 fromtimestamp = classmethod(fromtimestamp)
582 class Interval:
583 '''
584 Date intervals are specified using the suffixes "y", "m", and "d". The
585 suffix "w" (for "week") means 7 days. Time intervals are specified in
586 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
587 may not).
589 "3y" means three years
590 "2y 1m" means two years and one month
591 "1m 25d" means one month and 25 days
592 "2w 3d" means two weeks and three days
593 "1d 2:50" means one day, two hours, and 50 minutes
594 "14:00" means 14 hours
595 "0:04:33" means four minutes and 33 seconds
597 Example usage:
598 >>> Interval(" 3w 1 d 2:00")
599 <Interval + 22d 2:00>
600 >>> Date(". + 2d") + Interval("- 3w")
601 <Date 2000-06-07.00:34:02>
602 >>> Interval('1:59:59') + Interval('00:00:01')
603 <Interval + 2:00>
604 >>> Interval('2:00') + Interval('- 00:00:01')
605 <Interval + 1:59:59>
606 >>> Interval('1y')/2
607 <Interval + 6m>
608 >>> Interval('1:00')/2
609 <Interval + 0:30>
610 >>> Interval('2003-03-18')
611 <Interval + [number of days between now and 2003-03-18]>
612 >>> Interval('-4d 2003-03-18')
613 <Interval + [number of days between now and 2003-03-14]>
615 Interval arithmetic is handled in a couple of special ways, trying
616 to cater for the most common cases. Fundamentally, Intervals which
617 have both date and time parts will result in strange results in
618 arithmetic - because of the impossibility of handling day->month->year
619 over- and under-flows. Intervals may also be divided by some number.
621 Intervals are added to Dates in order of:
622 seconds, minutes, hours, years, months, days
624 Calculations involving months (eg '+2m') have no effect on days - only
625 days (or over/underflow from hours/mins/secs) will do that, and
626 days-per-month and leap years are accounted for. Leap seconds are not.
628 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
629 minute, second) is the serialisation format returned by the serialise()
630 method, and is accepted as an argument on instatiation.
632 TODO: more examples, showing the order of addition operation
633 '''
634 def __init__(self, spec, sign=1, allowdate=1, add_granularity=False,
635 translator=i18n
636 ):
637 """Construct an interval given a specification."""
638 self.setTranslator(translator)
639 if isinstance(spec, (int, float, long)):
640 self.from_seconds(spec)
641 elif isinstance(spec, basestring):
642 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
643 elif isinstance(spec, Interval):
644 (self.sign, self.year, self.month, self.day, self.hour,
645 self.minute, self.second) = spec.get_tuple()
646 else:
647 if len(spec) == 7:
648 self.sign, self.year, self.month, self.day, self.hour, \
649 self.minute, self.second = spec
650 self.second = int(self.second)
651 else:
652 # old, buggy spec form
653 self.sign = sign
654 self.year, self.month, self.day, self.hour, self.minute, \
655 self.second = spec
656 self.second = int(self.second)
658 def __deepcopy__(self, memo):
659 return Interval((self.sign, self.year, self.month, self.day,
660 self.hour, self.minute, self.second), translator=self.translator)
662 def set(self, spec, allowdate=1, interval_re=re.compile('''
663 \s*(?P<s>[-+])? # + or -
664 \s*((?P<y>\d+\s*)y)? # year
665 \s*((?P<m>\d+\s*)m)? # month
666 \s*((?P<w>\d+\s*)w)? # week
667 \s*((?P<d>\d+\s*)d)? # day
668 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
669 \s*(?P<D>
670 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
671 \.? # .
672 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
673 )?''', re.VERBOSE), serialised_re=re.compile('''
674 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
675 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
676 add_granularity=False):
677 ''' set the date to the value in spec
678 '''
679 self.year = self.month = self.week = self.day = self.hour = \
680 self.minute = self.second = 0
681 self.sign = 1
682 m = serialised_re.match(spec)
683 if not m:
684 m = interval_re.match(spec)
685 if not m:
686 raise ValueError, self._('Not an interval spec:'
687 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
688 else:
689 allowdate = 0
691 # pull out all the info specified
692 info = m.groupdict()
693 if add_granularity:
694 for gran in 'SMHdwmy':
695 if info[gran] is not None:
696 info[gran] = int(info[gran]) + (info['s']=='-' and -1 or 1)
697 break
699 valid = 0
700 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
701 'H':'hour', 'M':'minute', 'S':'second'}.items():
702 if info.get(group, None) is not None:
703 valid = 1
704 setattr(self, attr, int(info[group]))
706 # make sure it's valid
707 if not valid and not info['D']:
708 raise ValueError, self._('Not an interval spec:'
709 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
711 if self.week:
712 self.day = self.day + self.week*7
714 if info['s'] is not None:
715 self.sign = {'+':1, '-':-1}[info['s']]
717 # use a date spec if one is given
718 if allowdate and info['D'] is not None:
719 now = Date('.')
720 date = Date(info['D'])
721 # if no time part was specified, nuke it in the "now" date
722 if not date.hour or date.minute or date.second:
723 now.hour = now.minute = now.second = 0
724 if date != now:
725 y = now - (date + self)
726 self.__init__(y.get_tuple())
728 def __cmp__(self, other):
729 """Compare this interval to another interval."""
731 if other is None:
732 # we are always larger than None
733 return 1
734 return cmp(self.as_seconds(), other.as_seconds())
736 def __str__(self):
737 """Return this interval as a string."""
738 l = []
739 if self.year: l.append('%sy'%self.year)
740 if self.month: l.append('%sm'%self.month)
741 if self.day: l.append('%sd'%self.day)
742 if self.second:
743 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
744 elif self.hour or self.minute:
745 l.append('%d:%02d'%(self.hour, self.minute))
746 if l:
747 l.insert(0, {1:'+', -1:'-'}[self.sign])
748 else:
749 l.append('00:00')
750 return ' '.join(l)
752 def __add__(self, other):
753 if isinstance(other, Date):
754 # the other is a Date - produce a Date
755 return Date(other.addInterval(self), translator=self.translator)
756 elif isinstance(other, Interval):
757 # add the other Interval to this one
758 a = self.get_tuple()
759 asgn = a[0]
760 b = other.get_tuple()
761 bsgn = b[0]
762 i = [asgn*x + bsgn*y for x,y in zip(a[1:],b[1:])]
763 i.insert(0, 1)
764 i = fixTimeOverflow(i)
765 return Interval(i, translator=self.translator)
766 # nope, no idea what to do with this other...
767 raise TypeError, "Can't add %r"%other
769 def __sub__(self, other):
770 if isinstance(other, Date):
771 # the other is a Date - produce a Date
772 interval = Interval(self.get_tuple())
773 interval.sign *= -1
774 return Date(other.addInterval(interval),
775 translator=self.translator)
776 elif isinstance(other, Interval):
777 # add the other Interval to this one
778 a = self.get_tuple()
779 asgn = a[0]
780 b = other.get_tuple()
781 bsgn = b[0]
782 i = [asgn*x - bsgn*y for x,y in zip(a[1:],b[1:])]
783 i.insert(0, 1)
784 i = fixTimeOverflow(i)
785 return Interval(i, translator=self.translator)
786 # nope, no idea what to do with this other...
787 raise TypeError, "Can't add %r"%other
789 def __div__(self, other):
790 """ Divide this interval by an int value.
792 Can't divide years and months sensibly in the _same_
793 calculation as days/time, so raise an error in that situation.
794 """
795 try:
796 other = float(other)
797 except TypeError:
798 raise ValueError, "Can only divide Intervals by numbers"
800 y, m, d, H, M, S = (self.year, self.month, self.day,
801 self.hour, self.minute, self.second)
802 if y or m:
803 if d or H or M or S:
804 raise ValueError, "Can't divide Interval with date and time"
805 months = self.year*12 + self.month
806 months *= self.sign
808 months = int(months/other)
810 sign = months<0 and -1 or 1
811 m = months%12
812 y = months / 12
813 return Interval((sign, y, m, 0, 0, 0, 0),
814 translator=self.translator)
816 else:
817 # handle a day/time division
818 seconds = S + M*60 + H*60*60 + d*60*60*24
819 seconds *= self.sign
821 seconds = int(seconds/other)
823 sign = seconds<0 and -1 or 1
824 seconds *= sign
825 S = seconds%60
826 seconds /= 60
827 M = seconds%60
828 seconds /= 60
829 H = seconds%24
830 d = seconds / 24
831 return Interval((sign, 0, 0, d, H, M, S),
832 translator=self.translator)
834 def __repr__(self):
835 return '<Interval %s>'%self.__str__()
837 def pretty(self):
838 ''' print up the date date using one of these nice formats..
839 '''
840 _quarters = self.minute / 15
841 if self.year:
842 s = self.ngettext("%(number)s year", "%(number)s years",
843 self.year) % {'number': self.year}
844 elif self.month or self.day > 28:
845 _months = max(1, int(((self.month * 30) + self.day) / 30))
846 s = self.ngettext("%(number)s month", "%(number)s months",
847 _months) % {'number': _months}
848 elif self.day > 7:
849 _weeks = int(self.day / 7)
850 s = self.ngettext("%(number)s week", "%(number)s weeks",
851 _weeks) % {'number': _weeks}
852 elif self.day > 1:
853 # Note: singular form is not used
854 s = self.ngettext('%(number)s day', '%(number)s days',
855 self.day) % {'number': self.day}
856 elif self.day == 1 or self.hour > 12:
857 if self.sign > 0:
858 return self._('tomorrow')
859 else:
860 return self._('yesterday')
861 elif self.hour > 1:
862 # Note: singular form is not used
863 s = self.ngettext('%(number)s hour', '%(number)s hours',
864 self.hour) % {'number': self.hour}
865 elif self.hour == 1:
866 if self.minute < 15:
867 s = self._('an hour')
868 elif _quarters == 2:
869 s = self._('1 1/2 hours')
870 else:
871 s = self.ngettext('1 %(number)s/4 hours',
872 '1 %(number)s/4 hours', _quarters)%{'number': _quarters}
873 elif self.minute < 1:
874 if self.sign > 0:
875 return self._('in a moment')
876 else:
877 return self._('just now')
878 elif self.minute == 1:
879 # Note: used in expressions "in 1 minute" or "1 minute ago"
880 s = self._('1 minute')
881 elif self.minute < 15:
882 # Note: used in expressions "in 2 minutes" or "2 minutes ago"
883 s = self.ngettext('%(number)s minute', '%(number)s minutes',
884 self.minute) % {'number': self.minute}
885 elif _quarters == 2:
886 s = self._('1/2 an hour')
887 else:
888 s = self.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
889 _quarters) % {'number': _quarters}
890 # XXX this is internationally broken
891 if self.sign < 0:
892 s = self._('%s ago') % s
893 else:
894 s = self._('in %s') % s
895 return s
897 def get_tuple(self):
898 return (self.sign, self.year, self.month, self.day, self.hour,
899 self.minute, self.second)
901 def serialise(self):
902 sign = self.sign > 0 and '+' or '-'
903 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
904 self.day, self.hour, self.minute, self.second)
906 def as_seconds(self):
907 '''Calculate the Interval as a number of seconds.
909 Months are counted as 30 days, years as 365 days. Returns a Long
910 int.
911 '''
912 n = self.year * 365L
913 n = n + self.month * 30
914 n = n + self.day
915 n = n * 24
916 n = n + self.hour
917 n = n * 60
918 n = n + self.minute
919 n = n * 60
920 n = n + self.second
921 return n * self.sign
923 def from_seconds(self, val):
924 '''Figure my second, minute, hour and day values using a seconds
925 value.
926 '''
927 val = int(val)
928 if val < 0:
929 self.sign = -1
930 val = -val
931 else:
932 self.sign = 1
933 self.second = val % 60
934 val = val / 60
935 self.minute = val % 60
936 val = val / 60
937 self.hour = val % 24
938 val = val / 24
939 self.day = val
940 self.month = self.year = 0
942 def setTranslator(self, translator):
943 """Replace the translation engine
945 'translator'
946 is i18n module or one of gettext translation classes.
947 It must have attributes 'gettext' and 'ngettext',
948 serving as translation functions.
949 """
950 self.translator = translator
951 self._ = translator.gettext
952 self.ngettext = translator.ngettext
955 def fixTimeOverflow(time):
956 """ Handle the overflow in the time portion (H, M, S) of "time":
957 (sign, y,m,d,H,M,S)
959 Overflow and underflow will at most affect the _days_ portion of
960 the date. We do not overflow days to months as we don't know _how_
961 to, generally.
962 """
963 # XXX we could conceivably use this function for handling regular dates
964 # XXX too - we just need to interrogate the month/year for the day
965 # XXX overflow...
967 sign, y, m, d, H, M, S = time
968 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
969 if seconds:
970 sign = seconds<0 and -1 or 1
971 seconds *= sign
972 S = seconds%60
973 seconds /= 60
974 M = seconds%60
975 seconds /= 60
976 H = seconds%24
977 d = seconds / 24
978 else:
979 months = y*12 + m
980 sign = months<0 and -1 or 1
981 months *= sign
982 m = months%12
983 y = months/12
985 return (sign, y, m, d, H, M, S)
987 class Range:
988 """Represents range between two values
989 Ranges can be created using one of theese two alternative syntaxes:
991 1. Native english syntax::
993 [[From] <value>][ To <value>]
995 Keywords "From" and "To" are case insensitive. Keyword "From" is
996 optional.
998 2. "Geek" syntax::
1000 [<value>][; <value>]
1002 Either first or second <value> can be omitted in both syntaxes.
1004 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
1006 >>> Range("from 2-12 to 4-2")
1007 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
1009 >>> Range("18:00 TO +2m")
1010 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
1012 >>> Range("12:00")
1013 <Range from 2003-03-08.12:00:00 to None>
1015 >>> Range("tO +3d")
1016 <Range from None to 2003-03-11.20:07:48>
1018 >>> Range("2002-11-10; 2002-12-12")
1019 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
1021 >>> Range("; 20:00 +1d")
1022 <Range from None to 2003-03-09.20:00:00>
1024 """
1025 def __init__(self, spec, Type, allow_granularity=True, **params):
1026 """Initializes Range of type <Type> from given <spec> string.
1028 Sets two properties - from_value and to_value. None assigned to any of
1029 this properties means "infinitum" (-infinitum to from_value and
1030 +infinitum to to_value)
1032 The Type parameter here should be class itself (e.g. Date), not a
1033 class instance.
1034 """
1035 self.range_type = Type
1036 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
1037 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
1038 # Check which syntax to use
1039 if ';' in spec:
1040 # Geek
1041 m = re.search(re_geek_range, spec.strip())
1042 else:
1043 # Native english
1044 m = re.search(re_range, spec.strip(), re.IGNORECASE)
1045 if m:
1046 self.from_value, self.to_value = m.groups()
1047 if self.from_value:
1048 self.from_value = Type(self.from_value.strip(), **params)
1049 if self.to_value:
1050 self.to_value = Type(self.to_value.strip(), **params)
1051 else:
1052 if allow_granularity:
1053 self.from_value = Type(spec, **params)
1054 self.to_value = Type(spec, add_granularity=True, **params)
1055 else:
1056 raise ValueError, "Invalid range"
1058 def __str__(self):
1059 return "from %s to %s" % (self.from_value, self.to_value)
1061 def __repr__(self):
1062 return "<Range %s>" % self.__str__()
1064 def test_range():
1065 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
1066 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
1067 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
1068 for rspec in rspecs:
1069 print '>>> Range("%s")' % rspec
1070 print `Range(rspec, Date)`
1071 print
1072 for rspec in rispecs:
1073 print '>>> Range("%s")' % rspec
1074 print `Range(rspec, Interval)`
1075 print
1077 def test():
1078 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
1079 for interval in intervals:
1080 print '>>> Interval("%s")'%interval
1081 print `Interval(interval)`
1083 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
1084 "08-13.22:13", "14:25", '2002-12')
1085 for date in dates:
1086 print '>>> Date("%s")'%date
1087 print `Date(date)`
1089 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
1090 for date, interval in sums:
1091 print '>>> Date("%s") + Interval("%s")'%(date, interval)
1092 print `Date(date) + Interval(interval)`
1094 if __name__ == '__main__':
1095 test()
1097 # vim: set filetype=python sts=4 sw=4 et si :