Premiere version : mise en route du suivi.
[auf_roundup.git] / build / lib / roundup / date.py
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 $
19
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
23
24 import calendar
25 import datetime
26 import time
27 import re
28
29 try:
30 import pytz
31 except ImportError:
32 pytz = None
33
34 from roundup import i18n
35
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)
48
49 _timedelta0 = datetime.timedelta(0)
50
51 # load UTC tzinfo
52 if pytz:
53 UTC = pytz.utc
54 else:
55 # fallback implementation from Python Library Reference
56
57 class _UTC(datetime.tzinfo):
58
59 """Universal Coordinated Time zoneinfo"""
60
61 def utcoffset(self, dt):
62 return _timedelta0
63
64 def tzname(self, dt):
65 return "UTC"
66
67 def dst(self, dt):
68 return _timedelta0
69
70 def __repr__(self):
71 return "<UTC>"
72
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.
77
78 def normalize(self, dt, is_dst=False):
79 return dt.replace(tzinfo=self)
80
81 def localize(self, dt, is_dst=False):
82 return dt.replace(tzinfo=self)
83
84 UTC = _UTC()
85
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):
89
90 """Simple zoneinfo with fixed numeric offset and no daylight savings"""
91
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
99
100 def utcoffset(self, dt):
101 return datetime.timedelta(hours=self.offset)
102
103 def tzname(self, dt):
104 return self.name
105
106 def dst(self, dt):
107 return _timedelta0
108
109 def __repr__(self):
110 return "<%s: %s>" % (self.__class__.__name__, self.name)
111
112 # pytz adjustments interface
113
114 def normalize(self, dt):
115 return dt.replace(tzinfo=self)
116
117 def localize(self, dt, is_dst=False):
118 return dt.replace(tzinfo=self)
119
120 # simple timezones with fixed offset
121 _tzoffsets = dict(GMT=0, UCT=0, EST=5, MST=7, HST=10)
122
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
147
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)
155
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)
161
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.
172
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::
181
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"
192
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::
200
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>
215
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.
219
220 The date class handles basic arithmetic::
221
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 '''
236
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.
240
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,)
274
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 '''
279
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
288
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"')
295
296 info = m.groupdict()
297
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'))
316
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.
321
322 # whether we need to convert to UTC
323 adjust = False
324
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
339
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
348
349
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.
360
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,)
368
369 # adjust by added granularity
370 if add_granularity:
371 self.applyInterval(add_granularity)
372 self.applyInterval(Interval('- 00:00:01'))
373
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
386
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
397
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
402
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]
407
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
418
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
423
424 return (year, month, day, hour, minute, second, 0, 0, 0)
425
426 def differenceDate(self, other):
427 "Return the difference between this date and another date"
428 return self - other
429
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)
435
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)
440
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)
451
452 assert isinstance(other, Date), 'May only subtract Dates or Intervals'
453
454 return self.dateDelta(other)
455
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)
479
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)
494
495 def __str__(self):
496 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
497 return self.formal()
498
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)
503
504 def pretty(self, format='%d %B %Y'):
505 ''' print up the date date using a pretty format...
506
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)
514
515 # handle zero day by removing it
516 if format.startswith('%d') and str[0] == '0':
517 return ' ' + str[1:]
518 return str
519
520 def __repr__(self):
521 return '<Date %s>'%self.formal(sec='%06.3f')
522
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)
529
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)
533
534 def get_tuple(self):
535 return (self.year, self.month, self.day, self.hour, self.minute,
536 self.second, 0, 0, 0)
537
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)
541
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
549
550 def setTranslator(self, translator):
551 """Replace the translation engine
552
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
561
562 def fromtimestamp(cls, ts):
563 """Create a date object from a timestamp.
564
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)
572
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).
579
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
587
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]>
605
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.
611
612 Intervals are added to Dates in order of:
613 seconds, minutes, hours, years, months, days
614
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.
618
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.
622
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)
648
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)
652
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
681
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
689
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]))
696
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]')
701
702 if self.week:
703 self.day = self.day + self.week*7
704
705 if info['s'] is not None:
706 self.sign = {'+':1, '-':-1}[info['s']]
707
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())
718
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
729
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)
745
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
762
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
782
783 def __div__(self, other):
784 """ Divide this interval by an int value.
785
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"
793
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
801
802 months = int(months/other)
803
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)
809
810 else:
811 # handle a day/time division
812 seconds = S + M*60 + H*60*60 + d*60*60*24
813 seconds *= self.sign
814
815 seconds = int(seconds/other)
816
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)
827
828 def __repr__(self):
829 return '<Interval %s>'%self.__str__()
830
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
890
891 def get_tuple(self):
892 return (self.sign, self.year, self.month, self.day, self.hour,
893 self.minute, self.second)
894
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)
899
900 def as_seconds(self):
901 '''Calculate the Interval as a number of seconds.
902
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
916
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
935
936 def setTranslator(self, translator):
937 """Replace the translation engine
938
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
947
948
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)
952
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...
960
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
978
979 return (sign, y, m, d, H, M, S)
980
981 class Range:
982 """Represents range between two values
983 Ranges can be created using one of theese two alternative syntaxes:
984
985 1. Native english syntax::
986
987 [[From] <value>][ To <value>]
988
989 Keywords "From" and "To" are case insensitive. Keyword "From" is
990 optional.
991
992 2. "Geek" syntax::
993
994 [<value>][; <value>]
995
996 Either first or second <value> can be omitted in both syntaxes.
997
998 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
999
1000 >>> Range("from 2-12 to 4-2")
1001 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
1002
1003 >>> Range("18:00 TO +2m")
1004 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
1005
1006 >>> Range("12:00")
1007 <Range from 2003-03-08.12:00:00 to None>
1008
1009 >>> Range("tO +3d")
1010 <Range from None to 2003-03-11.20:07:48>
1011
1012 >>> Range("2002-11-10; 2002-12-12")
1013 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
1014
1015 >>> Range("; 20:00 +1d")
1016 <Range from None to 2003-03-09.20:00:00>
1017
1018 """
1019 def __init__(self, spec, Type, allow_granularity=True, **params):
1020 """Initializes Range of type <Type> from given <spec> string.
1021
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)
1025
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"
1051
1052 def __str__(self):
1053 return "from %s to %s" % (self.from_value, self.to_value)
1054
1055 def __repr__(self):
1056 return "<Range %s>" % self.__str__()
1057
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
1070
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)`
1076
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)`
1082
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)`
1087
1088 if __name__ == '__main__':
1089 test()
1090
1091 # vim: set filetype=python sts=4 sw=4 et si :