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