f857bf7c969103488280ffa7777a8352a6248607
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 """
526 y, m, d, H, M, S = _utc_to_local(self.year, self.month, self.day,
527 self.hour, self.minute, self.second, offset)
528 return Date((y, m, d, H, M, S, 0, 0, 0), translator=self.translator)
530 def __deepcopy__(self, memo):
531 return Date((self.year, self.month, self.day, self.hour,
532 self.minute, self.second, 0, 0, 0), translator=self.translator)
534 def get_tuple(self):
535 return (self.year, self.month, self.day, self.hour, self.minute,
536 self.second, 0, 0, 0)
538 def serialise(self):
539 return '%04d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
540 self.day, self.hour, self.minute, self.second)
542 def timestamp(self):
543 ''' return a UNIX timestamp for this date '''
544 frac = self.second - int(self.second)
545 ts = calendar.timegm((self.year, self.month, self.day, self.hour,
546 self.minute, self.second, 0, 0, 0))
547 # we lose the fractional part
548 return ts + frac
550 def setTranslator(self, translator):
551 """Replace the translation engine
553 'translator'
554 is i18n module or one of gettext translation classes.
555 It must have attributes 'gettext' and 'ngettext',
556 serving as translation functions.
557 """
558 self.translator = translator
559 self._ = translator.gettext
560 self.ngettext = translator.ngettext
562 def fromtimestamp(cls, ts):
563 """Create a date object from a timestamp.
565 The timestamp may be outside the gmtime year-range of
566 1902-2038.
567 """
568 usec = int((ts - int(ts)) * 1000000.)
569 delta = datetime.timedelta(seconds = int(ts), microseconds = usec)
570 return cls(datetime.datetime(1970, 1, 1) + delta)
571 fromtimestamp = classmethod(fromtimestamp)
573 class Interval:
574 '''
575 Date intervals are specified using the suffixes "y", "m", and "d". The
576 suffix "w" (for "week") means 7 days. Time intervals are specified in
577 hh:mm:ss format (the seconds may be omitted, but the hours and minutes
578 may not).
580 "3y" means three years
581 "2y 1m" means two years and one month
582 "1m 25d" means one month and 25 days
583 "2w 3d" means two weeks and three days
584 "1d 2:50" means one day, two hours, and 50 minutes
585 "14:00" means 14 hours
586 "0:04:33" means four minutes and 33 seconds
588 Example usage:
589 >>> Interval(" 3w 1 d 2:00")
590 <Interval + 22d 2:00>
591 >>> Date(". + 2d") + Interval("- 3w")
592 <Date 2000-06-07.00:34:02>
593 >>> Interval('1:59:59') + Interval('00:00:01')
594 <Interval + 2:00>
595 >>> Interval('2:00') + Interval('- 00:00:01')
596 <Interval + 1:59:59>
597 >>> Interval('1y')/2
598 <Interval + 6m>
599 >>> Interval('1:00')/2
600 <Interval + 0:30>
601 >>> Interval('2003-03-18')
602 <Interval + [number of days between now and 2003-03-18]>
603 >>> Interval('-4d 2003-03-18')
604 <Interval + [number of days between now and 2003-03-14]>
606 Interval arithmetic is handled in a couple of special ways, trying
607 to cater for the most common cases. Fundamentally, Intervals which
608 have both date and time parts will result in strange results in
609 arithmetic - because of the impossibility of handling day->month->year
610 over- and under-flows. Intervals may also be divided by some number.
612 Intervals are added to Dates in order of:
613 seconds, minutes, hours, years, months, days
615 Calculations involving months (eg '+2m') have no effect on days - only
616 days (or over/underflow from hours/mins/secs) will do that, and
617 days-per-month and leap years are accounted for. Leap seconds are not.
619 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
620 minute, second) is the serialisation format returned by the serialise()
621 method, and is accepted as an argument on instatiation.
623 TODO: more examples, showing the order of addition operation
624 '''
625 def __init__(self, spec, sign=1, allowdate=1, add_granularity=False,
626 translator=i18n
627 ):
628 """Construct an interval given a specification."""
629 self.setTranslator(translator)
630 if isinstance(spec, (int, float, long)):
631 self.from_seconds(spec)
632 elif isinstance(spec, basestring):
633 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
634 elif isinstance(spec, Interval):
635 (self.sign, self.year, self.month, self.day, self.hour,
636 self.minute, self.second) = spec.get_tuple()
637 else:
638 if len(spec) == 7:
639 self.sign, self.year, self.month, self.day, self.hour, \
640 self.minute, self.second = spec
641 self.second = int(self.second)
642 else:
643 # old, buggy spec form
644 self.sign = sign
645 self.year, self.month, self.day, self.hour, self.minute, \
646 self.second = spec
647 self.second = int(self.second)
649 def __deepcopy__(self, memo):
650 return Interval((self.sign, self.year, self.month, self.day,
651 self.hour, self.minute, self.second), translator=self.translator)
653 def set(self, spec, allowdate=1, interval_re=re.compile('''
654 \s*(?P<s>[-+])? # + or -
655 \s*((?P<y>\d+\s*)y)? # year
656 \s*((?P<m>\d+\s*)m)? # month
657 \s*((?P<w>\d+\s*)w)? # week
658 \s*((?P<d>\d+\s*)d)? # day
659 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
660 \s*(?P<D>
661 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
662 \.? # .
663 (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
664 )?''', re.VERBOSE), serialised_re=re.compile('''
665 (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
666 (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
667 add_granularity=False):
668 ''' set the date to the value in spec
669 '''
670 self.year = self.month = self.week = self.day = self.hour = \
671 self.minute = self.second = 0
672 self.sign = 1
673 m = serialised_re.match(spec)
674 if not m:
675 m = interval_re.match(spec)
676 if not m:
677 raise ValueError, self._('Not an interval spec:'
678 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
679 else:
680 allowdate = 0
682 # pull out all the info specified
683 info = m.groupdict()
684 if add_granularity:
685 for gran in 'SMHdwmy':
686 if info[gran] is not None:
687 info[gran] = int(info[gran]) + (info['s']=='-' and -1 or 1)
688 break
690 valid = 0
691 for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
692 'H':'hour', 'M':'minute', 'S':'second'}.items():
693 if info.get(group, None) is not None:
694 valid = 1
695 setattr(self, attr, int(info[group]))
697 # make sure it's valid
698 if not valid and not info['D']:
699 raise ValueError, self._('Not an interval spec:'
700 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
702 if self.week:
703 self.day = self.day + self.week*7
705 if info['s'] is not None:
706 self.sign = {'+':1, '-':-1}[info['s']]
708 # use a date spec if one is given
709 if allowdate and info['D'] is not None:
710 now = Date('.')
711 date = Date(info['D'])
712 # if no time part was specified, nuke it in the "now" date
713 if not date.hour or date.minute or date.second:
714 now.hour = now.minute = now.second = 0
715 if date != now:
716 y = now - (date + self)
717 self.__init__(y.get_tuple())
719 def __cmp__(self, other):
720 """Compare this interval to another interval."""
721 if other is None:
722 # we are always larger than None
723 return 1
724 for attr in 'sign year month day hour minute second'.split():
725 r = cmp(getattr(self, attr), getattr(other, attr))
726 if r:
727 return r
728 return 0
730 def __str__(self):
731 """Return this interval as a string."""
732 l = []
733 if self.year: l.append('%sy'%self.year)
734 if self.month: l.append('%sm'%self.month)
735 if self.day: l.append('%sd'%self.day)
736 if self.second:
737 l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
738 elif self.hour or self.minute:
739 l.append('%d:%02d'%(self.hour, self.minute))
740 if l:
741 l.insert(0, {1:'+', -1:'-'}[self.sign])
742 else:
743 l.append('00:00')
744 return ' '.join(l)
746 def __add__(self, other):
747 if isinstance(other, Date):
748 # the other is a Date - produce a Date
749 return Date(other.addInterval(self), translator=self.translator)
750 elif isinstance(other, Interval):
751 # add the other Interval to this one
752 a = self.get_tuple()
753 asgn = a[0]
754 b = other.get_tuple()
755 bsgn = b[0]
756 i = [asgn*x + bsgn*y for x,y in zip(a[1:],b[1:])]
757 i.insert(0, 1)
758 i = fixTimeOverflow(i)
759 return Interval(i, translator=self.translator)
760 # nope, no idea what to do with this other...
761 raise TypeError, "Can't add %r"%other
763 def __sub__(self, other):
764 if isinstance(other, Date):
765 # the other is a Date - produce a Date
766 interval = Interval(self.get_tuple())
767 interval.sign *= -1
768 return Date(other.addInterval(interval),
769 translator=self.translator)
770 elif isinstance(other, Interval):
771 # add the other Interval to this one
772 a = self.get_tuple()
773 asgn = a[0]
774 b = other.get_tuple()
775 bsgn = b[0]
776 i = [asgn*x - bsgn*y for x,y in zip(a[1:],b[1:])]
777 i.insert(0, 1)
778 i = fixTimeOverflow(i)
779 return Interval(i, translator=self.translator)
780 # nope, no idea what to do with this other...
781 raise TypeError, "Can't add %r"%other
783 def __div__(self, other):
784 """ Divide this interval by an int value.
786 Can't divide years and months sensibly in the _same_
787 calculation as days/time, so raise an error in that situation.
788 """
789 try:
790 other = float(other)
791 except TypeError:
792 raise ValueError, "Can only divide Intervals by numbers"
794 y, m, d, H, M, S = (self.year, self.month, self.day,
795 self.hour, self.minute, self.second)
796 if y or m:
797 if d or H or M or S:
798 raise ValueError, "Can't divide Interval with date and time"
799 months = self.year*12 + self.month
800 months *= self.sign
802 months = int(months/other)
804 sign = months<0 and -1 or 1
805 m = months%12
806 y = months / 12
807 return Interval((sign, y, m, 0, 0, 0, 0),
808 translator=self.translator)
810 else:
811 # handle a day/time division
812 seconds = S + M*60 + H*60*60 + d*60*60*24
813 seconds *= self.sign
815 seconds = int(seconds/other)
817 sign = seconds<0 and -1 or 1
818 seconds *= sign
819 S = seconds%60
820 seconds /= 60
821 M = seconds%60
822 seconds /= 60
823 H = seconds%24
824 d = seconds / 24
825 return Interval((sign, 0, 0, d, H, M, S),
826 translator=self.translator)
828 def __repr__(self):
829 return '<Interval %s>'%self.__str__()
831 def pretty(self):
832 ''' print up the date date using one of these nice formats..
833 '''
834 _quarters = self.minute / 15
835 if self.year:
836 s = self.ngettext("%(number)s year", "%(number)s years",
837 self.year) % {'number': self.year}
838 elif self.month or self.day > 28:
839 _months = max(1, int(((self.month * 30) + self.day) / 30))
840 s = self.ngettext("%(number)s month", "%(number)s months",
841 _months) % {'number': _months}
842 elif self.day > 7:
843 _weeks = int(self.day / 7)
844 s = self.ngettext("%(number)s week", "%(number)s weeks",
845 _weeks) % {'number': _weeks}
846 elif self.day > 1:
847 # Note: singular form is not used
848 s = self.ngettext('%(number)s day', '%(number)s days',
849 self.day) % {'number': self.day}
850 elif self.day == 1 or self.hour > 12:
851 if self.sign > 0:
852 return self._('tomorrow')
853 else:
854 return self._('yesterday')
855 elif self.hour > 1:
856 # Note: singular form is not used
857 s = self.ngettext('%(number)s hour', '%(number)s hours',
858 self.hour) % {'number': self.hour}
859 elif self.hour == 1:
860 if self.minute < 15:
861 s = self._('an hour')
862 elif _quarters == 2:
863 s = self._('1 1/2 hours')
864 else:
865 s = self.ngettext('1 %(number)s/4 hours',
866 '1 %(number)s/4 hours', _quarters)%{'number': _quarters}
867 elif self.minute < 1:
868 if self.sign > 0:
869 return self._('in a moment')
870 else:
871 return self._('just now')
872 elif self.minute == 1:
873 # Note: used in expressions "in 1 minute" or "1 minute ago"
874 s = self._('1 minute')
875 elif self.minute < 15:
876 # Note: used in expressions "in 2 minutes" or "2 minutes ago"
877 s = self.ngettext('%(number)s minute', '%(number)s minutes',
878 self.minute) % {'number': self.minute}
879 elif _quarters == 2:
880 s = self._('1/2 an hour')
881 else:
882 s = self.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
883 _quarters) % {'number': _quarters}
884 # XXX this is internationally broken
885 if self.sign < 0:
886 s = self._('%s ago') % s
887 else:
888 s = self._('in %s') % s
889 return s
891 def get_tuple(self):
892 return (self.sign, self.year, self.month, self.day, self.hour,
893 self.minute, self.second)
895 def serialise(self):
896 sign = self.sign > 0 and '+' or '-'
897 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
898 self.day, self.hour, self.minute, self.second)
900 def as_seconds(self):
901 '''Calculate the Interval as a number of seconds.
903 Months are counted as 30 days, years as 365 days. Returns a Long
904 int.
905 '''
906 n = self.year * 365L
907 n = n + self.month * 30
908 n = n + self.day
909 n = n * 24
910 n = n + self.hour
911 n = n * 60
912 n = n + self.minute
913 n = n * 60
914 n = n + self.second
915 return n * self.sign
917 def from_seconds(self, val):
918 '''Figure my second, minute, hour and day values using a seconds
919 value.
920 '''
921 val = int(val)
922 if val < 0:
923 self.sign = -1
924 val = -val
925 else:
926 self.sign = 1
927 self.second = val % 60
928 val = val / 60
929 self.minute = val % 60
930 val = val / 60
931 self.hour = val % 24
932 val = val / 24
933 self.day = val
934 self.month = self.year = 0
936 def setTranslator(self, translator):
937 """Replace the translation engine
939 'translator'
940 is i18n module or one of gettext translation classes.
941 It must have attributes 'gettext' and 'ngettext',
942 serving as translation functions.
943 """
944 self.translator = translator
945 self._ = translator.gettext
946 self.ngettext = translator.ngettext
949 def fixTimeOverflow(time):
950 """ Handle the overflow in the time portion (H, M, S) of "time":
951 (sign, y,m,d,H,M,S)
953 Overflow and underflow will at most affect the _days_ portion of
954 the date. We do not overflow days to months as we don't know _how_
955 to, generally.
956 """
957 # XXX we could conceivably use this function for handling regular dates
958 # XXX too - we just need to interrogate the month/year for the day
959 # XXX overflow...
961 sign, y, m, d, H, M, S = time
962 seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
963 if seconds:
964 sign = seconds<0 and -1 or 1
965 seconds *= sign
966 S = seconds%60
967 seconds /= 60
968 M = seconds%60
969 seconds /= 60
970 H = seconds%24
971 d = seconds / 24
972 else:
973 months = y*12 + m
974 sign = months<0 and -1 or 1
975 months *= sign
976 m = months%12
977 y = months/12
979 return (sign, y, m, d, H, M, S)
981 class Range:
982 """Represents range between two values
983 Ranges can be created using one of theese two alternative syntaxes:
985 1. Native english syntax::
987 [[From] <value>][ To <value>]
989 Keywords "From" and "To" are case insensitive. Keyword "From" is
990 optional.
992 2. "Geek" syntax::
994 [<value>][; <value>]
996 Either first or second <value> can be omitted in both syntaxes.
998 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
1000 >>> Range("from 2-12 to 4-2")
1001 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
1003 >>> Range("18:00 TO +2m")
1004 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
1006 >>> Range("12:00")
1007 <Range from 2003-03-08.12:00:00 to None>
1009 >>> Range("tO +3d")
1010 <Range from None to 2003-03-11.20:07:48>
1012 >>> Range("2002-11-10; 2002-12-12")
1013 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
1015 >>> Range("; 20:00 +1d")
1016 <Range from None to 2003-03-09.20:00:00>
1018 """
1019 def __init__(self, spec, Type, allow_granularity=True, **params):
1020 """Initializes Range of type <Type> from given <spec> string.
1022 Sets two properties - from_value and to_value. None assigned to any of
1023 this properties means "infinitum" (-infinitum to from_value and
1024 +infinitum to to_value)
1026 The Type parameter here should be class itself (e.g. Date), not a
1027 class instance.
1028 """
1029 self.range_type = Type
1030 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
1031 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
1032 # Check which syntax to use
1033 if ';' in spec:
1034 # Geek
1035 m = re.search(re_geek_range, spec.strip())
1036 else:
1037 # Native english
1038 m = re.search(re_range, spec.strip(), re.IGNORECASE)
1039 if m:
1040 self.from_value, self.to_value = m.groups()
1041 if self.from_value:
1042 self.from_value = Type(self.from_value.strip(), **params)
1043 if self.to_value:
1044 self.to_value = Type(self.to_value.strip(), **params)
1045 else:
1046 if allow_granularity:
1047 self.from_value = Type(spec, **params)
1048 self.to_value = Type(spec, add_granularity=True, **params)
1049 else:
1050 raise ValueError, "Invalid range"
1052 def __str__(self):
1053 return "from %s to %s" % (self.from_value, self.to_value)
1055 def __repr__(self):
1056 return "<Range %s>" % self.__str__()
1058 def test_range():
1059 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
1060 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
1061 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
1062 for rspec in rspecs:
1063 print '>>> Range("%s")' % rspec
1064 print `Range(rspec, Date)`
1065 print
1066 for rspec in rispecs:
1067 print '>>> Range("%s")' % rspec
1068 print `Range(rspec, Interval)`
1069 print
1071 def test():
1072 intervals = (" 3w 1 d 2:00", " + 2d", "3w")
1073 for interval in intervals:
1074 print '>>> Interval("%s")'%interval
1075 print `Interval(interval)`
1077 dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
1078 "08-13.22:13", "14:25", '2002-12')
1079 for date in dates:
1080 print '>>> Date("%s")'%date
1081 print `Date(date)`
1083 sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
1084 for date, interval in sums:
1085 print '>>> Date("%s") + Interval("%s")'%(date, interval)
1086 print `Date(date) + Interval(interval)`
1088 if __name__ == '__main__':
1089 test()
1091 # vim: set filetype=python sts=4 sw=4 et si :