Code

fix Link HTML field()
[roundup.git] / 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.63 2004-03-24 04:57:25 richard Exp $
20 """Date, time and time interval handling.
21 """
22 __docformat__ = 'restructuredtext'
24 import time, re, calendar, types
25 from i18n import _
27 def _add_granularity(src, order, value = 1):
28     '''Increment first non-None value in src dictionary ordered by 'order'
29     parameter
30     '''
31     for gran in order:
32         if src[gran]:
33             src[gran] = int(src[gran]) + value
34             break
36 class Date:
37     '''
38     As strings, date-and-time stamps are specified with the date in
39     international standard format (yyyy-mm-dd) joined to the time
40     (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
41     and are fairly readable when printed. An example of a valid stamp is
42     "2000-06-24.13:03:59". We'll call this the "full date format". When
43     Timestamp objects are printed as strings, they appear in the full date
44     format with the time always given in GMT. The full date format is
45     always exactly 19 characters long. 
47     For user input, some partial forms are also permitted: the whole time
48     or just the seconds may be omitted; and the whole date may be omitted
49     or just the year may be omitted. If the time is given, the time is
50     interpreted in the user's local time zone. The Date constructor takes
51     care of these conversions. In the following examples, suppose that yyyy
52     is the current year, mm is the current month, and dd is the current day
53     of the month; and suppose that the user is on Eastern Standard Time.
54     Examples::
56       "2000-04-17" means <Date 2000-04-17.00:00:00>
57       "01-25" means <Date yyyy-01-25.00:00:00>
58       "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
59       "08-13.22:13" means <Date yyyy-08-14.03:13:00>
60       "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
61       "14:25" means <Date yyyy-mm-dd.19:25:00>
62       "8:47:11" means <Date yyyy-mm-dd.13:47:11>
63       "2003" means <Date 2003-01-01.00:00:00>
64       "2003-06" means <Date 2003-06-01.00:00:00>
65       "." means "right now"
67     The Date class should understand simple date expressions of the form
68     stamp + interval and stamp - interval. When adding or subtracting
69     intervals involving months or years, the components are handled
70     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
71     first add one month to get 2000-07-25, then add 10 days to get
72     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
73     or 41 days).  Example usage::
75         >>> Date(".")
76         <Date 2000-06-26.00:34:02>
77         >>> _.local(-5)
78         "2000-06-25.19:34:02"
79         >>> Date(". + 2d")
80         <Date 2000-06-28.00:34:02>
81         >>> Date("1997-04-17", -5)
82         <Date 1997-04-17.00:00:00>
83         >>> Date("01-25", -5)
84         <Date 2000-01-25.00:00:00>
85         >>> Date("08-13.22:13", -5)
86         <Date 2000-08-14.03:13:00>
87         >>> Date("14:25", -5)
88         <Date 2000-06-25.19:25:00>
90     The date format 'yyyymmddHHMMSS' (year, month, day, hour,
91     minute, second) is the serialisation format returned by the serialise()
92     method, and is accepted as an argument on instatiation.
93     '''
94     
95     def __init__(self, spec='.', offset=0, add_granularity=0):
96         """Construct a date given a specification and a time zone offset.
98         'spec'
99            is a full date or a partial form, with an optional added or
100            subtracted interval. Or a date 9-tuple.
101         'offset'
102            is the local time zone offset from GMT in hours.
103         """
104         if type(spec) == type(''):
105             self.set(spec, offset=offset, add_granularity=add_granularity)
106             return
107         elif hasattr(spec, 'tuple'):
108             spec = spec.tuple()
109         try:
110             y,m,d,H,M,S,x,x,x = spec
111             frac = S - int(S)
112             ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
113             self.year, self.month, self.day, self.hour, self.minute, \
114                 self.second, x, x, x = time.gmtime(ts)
115             # we lost the fractional part
116             self.second = self.second + frac
117         except:
118             raise ValueError, 'Unknown spec %r'%spec
120     usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS.SSS][offset]'
121     def set(self, spec, offset=0, date_re=re.compile(r'''
122             ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
123             |(?P<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
124             (?P<n>\.)?                                     # .
125             (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d(\.\d+)?))?)?  # hh:mm:ss
126             (?P<o>.+)?                                     # offset
127             ''', re.VERBOSE), serialised_re=re.compile(r'''
128             (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d(\.\d+)?)
129             ''', re.VERBOSE), add_granularity=0):
130         ''' set the date to the value in spec
131         '''
133         m = serialised_re.match(spec)
134         if m is not None:
135             # we're serialised - easy!
136             g = m.groups()
137             (self.year, self.month, self.day, self.hour, self.minute) = \
138                 map(int, g[:5])
139             self.second = float(g[5])
140             return
142         # not serialised data, try usual format
143         m = date_re.match(spec)
144         if m is None:
145             raise ValueError, _('Not a date spec: %s' % self.usagespec)
147         info = m.groupdict()
149         if add_granularity:
150             _add_granularity(info, 'SMHdmyab')
152         # get the current date as our default
153         ts = time.time()
154         frac = ts - int(ts)
155         y,m,d,H,M,S,x,x,x = time.gmtime(ts)
156         # gmtime loses the fractional seconds 
157         S = S + frac
159         if info['y'] is not None or info['a'] is not None:
160             if info['y'] is not None:
161                 y = int(info['y'])
162                 m,d = (1,1)
163                 if info['m'] is not None:
164                     m = int(info['m'])
165                     if info['d'] is not None:
166                         d = int(info['d'])
167             if info['a'] is not None:
168                 m = int(info['a'])
169                 d = int(info['b'])
170             H = -offset
171             M = S = 0
173         # override hour, minute, second parts
174         if info['H'] is not None and info['M'] is not None:
175             H = int(info['H']) - offset
176             M = int(info['M'])
177             S = 0
178             if info['S'] is not None:
179                 S = float(info['S'])
181         if add_granularity:
182             S = S - 1
183         
184         # now handle the adjustment of hour
185         frac = S - int(S)
186         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
187         self.year, self.month, self.day, self.hour, self.minute, \
188             self.second, x, x, x = time.gmtime(ts)
189         # we lost the fractional part along the way
190         self.second = self.second + frac
192         if info.get('o', None):
193             try:
194                 self.applyInterval(Interval(info['o'], allowdate=0))
195             except ValueError:
196                 raise ValueError, _('%r not a date spec (%s)')%(spec,
197                     self.usagespec)
199     def addInterval(self, interval):
200         ''' Add the interval to this date, returning the date tuple
201         '''
202         # do the basic calc
203         sign = interval.sign
204         year = self.year + sign * interval.year
205         month = self.month + sign * interval.month
206         day = self.day + sign * interval.day
207         hour = self.hour + sign * interval.hour
208         minute = self.minute + sign * interval.minute
209         # Intervals work on whole seconds
210         second = int(self.second) + sign * interval.second
212         # now cope with under- and over-flow
213         # first do the time
214         while (second < 0 or second > 59 or minute < 0 or minute > 59 or
215                 hour < 0 or hour > 23):
216             if second < 0: minute -= 1; second += 60
217             elif second > 59: minute += 1; second -= 60
218             if minute < 0: hour -= 1; minute += 60
219             elif minute > 59: hour += 1; minute -= 60
220             if hour < 0: day -= 1; hour += 24
221             elif hour > 23: day += 1; hour -= 24
223         # fix up the month so we're within range
224         while month < 1 or month > 12:
225             if month < 1: year -= 1; month += 12
226             if month > 12: year += 1; month -= 12
228         # now do the days, now that we know what month we're in
229         def get_mdays(year, month):
230             if month == 2 and calendar.isleap(year): return 29
231             else: return calendar.mdays[month]
233         while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
234             # now to day under/over
235             if day < 1: 
236                 # When going backwards, decrement month, then increment days
237                 month -= 1
238                 day += get_mdays(year,month)
239             elif day > get_mdays(year,month): 
240                 # When going forwards, decrement days, then increment month
241                 day -= get_mdays(year,month)
242                 month += 1
244             # possibly fix up the month so we're within range
245             while month < 1 or month > 12:
246                 if month < 1: year -= 1; month += 12 ; day += 31
247                 if month > 12: year += 1; month -= 12
249         return (year, month, day, hour, minute, second, 0, 0, 0)
251     def differenceDate(self, other):
252         "Return the difference between this date and another date"
254     def applyInterval(self, interval):
255         ''' Apply the interval to this date
256         '''
257         self.year, self.month, self.day, self.hour, self.minute, \
258             self.second, x, x, x = self.addInterval(interval)
260     def __add__(self, interval):
261         """Add an interval to this date to produce another date.
262         """
263         return Date(self.addInterval(interval))
265     # deviates from spec to allow subtraction of dates as well
266     def __sub__(self, other):
267         """ Subtract:
268              1. an interval from this date to produce another date.
269              2. a date from this date to produce an interval.
270         """
271         if isinstance(other, Interval):
272             other = Interval(other.get_tuple())
273             other.sign *= -1
274             return self.__add__(other)
276         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
278         return self.dateDelta(other)
280     def dateDelta(self, other):
281         """ Produce an Interval of the difference between this date
282             and another date. Only returns days:hours:minutes:seconds.
283         """
284         # Returning intervals larger than a day is almost
285         # impossible - months, years, weeks, are all so imprecise.
286         a = calendar.timegm((self.year, self.month, self.day, self.hour,
287             self.minute, self.second, 0, 0, 0))
288         b = calendar.timegm((other.year, other.month, other.day,
289             other.hour, other.minute, other.second, 0, 0, 0))
290         # intervals work in whole seconds
291         diff = int(a - b)
292         if diff > 0:
293             sign = 1
294         else:
295             sign = -1
296             diff = -diff
297         S = diff%60
298         M = (diff/60)%60
299         H = (diff/(60*60))%24
300         d = diff/(24*60*60)
301         return Interval((0, 0, d, H, M, S), sign=sign)
303     def __cmp__(self, other, int_seconds=0):
304         """Compare this date to another date."""
305         if other is None:
306             return 1
307         for attr in ('year', 'month', 'day', 'hour', 'minute'):
308             if not hasattr(other, attr):
309                 return 1
310             r = cmp(getattr(self, attr), getattr(other, attr))
311             if r: return r
312         if not hasattr(other, 'second'):
313             return 1
314         if int_seconds:
315             return cmp(int(self.second), int(other.second))
316         return cmp(self.second, other.second)
318     def __str__(self):
319         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
320         return self.formal()
322     def formal(self, sep='.', sec='%02d'):
323         f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
324         return f%(self.year, self.month, self.day, self.hour, self.minute,
325             self.second)
327     def pretty(self, format='%d %B %Y'):
328         ''' print up the date date using a pretty format...
330             Note that if the day is zero, and the day appears first in the
331             format, then the day number will be removed from output.
332         '''
333         str = time.strftime(format, (self.year, self.month, self.day,
334             self.hour, self.minute, self.second, 0, 0, 0))
335         # handle zero day by removing it
336         if format.startswith('%d') and str[0] == '0':
337             return ' ' + str[1:]
338         return str
340     def __repr__(self):
341         return '<Date %s>'%self.formal(sec='%f')
343     def local(self, offset):
344         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
345         """
346         return Date((self.year, self.month, self.day, self.hour + offset,
347             self.minute, self.second, 0, 0, 0))
349     def get_tuple(self):
350         return (self.year, self.month, self.day, self.hour, self.minute,
351             self.second, 0, 0, 0)
353     def serialise(self):
354         return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
355             self.day, self.hour, self.minute, self.second)
357     def timestamp(self):
358         ''' return a UNIX timestamp for this date '''
359         frac = self.second - int(self.second)
360         ts = calendar.timegm((self.year, self.month, self.day, self.hour,
361             self.minute, self.second, 0, 0, 0))
362         # we lose the fractional part
363         return ts + frac
365 class Interval:
366     '''
367     Date intervals are specified using the suffixes "y", "m", and "d". The
368     suffix "w" (for "week") means 7 days. Time intervals are specified in
369     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
370     may not).
372       "3y" means three years
373       "2y 1m" means two years and one month
374       "1m 25d" means one month and 25 days
375       "2w 3d" means two weeks and three days
376       "1d 2:50" means one day, two hours, and 50 minutes
377       "14:00" means 14 hours
378       "0:04:33" means four minutes and 33 seconds
380     Example usage:
381         >>> Interval("  3w  1  d  2:00")
382         <Interval + 22d 2:00>
383         >>> Date(". + 2d") + Interval("- 3w")
384         <Date 2000-06-07.00:34:02>
385         >>> Interval('1:59:59') + Interval('00:00:01')
386         <Interval + 2:00>
387         >>> Interval('2:00') + Interval('- 00:00:01')
388         <Interval + 1:59:59>
389         >>> Interval('1y')/2
390         <Interval + 6m>
391         >>> Interval('1:00')/2
392         <Interval + 0:30>
393         >>> Interval('2003-03-18')
394         <Interval + [number of days between now and 2003-03-18]>
395         >>> Interval('-4d 2003-03-18')
396         <Interval + [number of days between now and 2003-03-14]>
398     Interval arithmetic is handled in a couple of special ways, trying
399     to cater for the most common cases. Fundamentally, Intervals which
400     have both date and time parts will result in strange results in
401     arithmetic - because of the impossibility of handling day->month->year
402     over- and under-flows. Intervals may also be divided by some number.
404     Intervals are added to Dates in order of:
405        seconds, minutes, hours, years, months, days
407     Calculations involving months (eg '+2m') have no effect on days - only
408     days (or over/underflow from hours/mins/secs) will do that, and
409     days-per-month and leap years are accounted for. Leap seconds are not.
411     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
412     minute, second) is the serialisation format returned by the serialise()
413     method, and is accepted as an argument on instatiation.
415     TODO: more examples, showing the order of addition operation
416     '''
417     def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
418         """Construct an interval given a specification."""
419         if type(spec) == type(''):
420             self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
421         else:
422             if len(spec) == 7:
423                 self.sign, self.year, self.month, self.day, self.hour, \
424                     self.minute, self.second = spec
425                 self.second = int(self.second)
426             else:
427                 # old, buggy spec form
428                 self.sign = sign
429                 self.year, self.month, self.day, self.hour, self.minute, \
430                     self.second = spec
431                 self.second = int(self.second)
433     def set(self, spec, allowdate=1, interval_re=re.compile('''
434             \s*(?P<s>[-+])?         # + or -
435             \s*((?P<y>\d+\s*)y)?    # year
436             \s*((?P<m>\d+\s*)m)?    # month
437             \s*((?P<w>\d+\s*)w)?    # week
438             \s*((?P<d>\d+\s*)d)?    # day
439             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
440             \s*(?P<D>
441                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
442                  \.?                                       # .
443                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
444                )?''', re.VERBOSE), serialised_re=re.compile('''
445             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
446             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
447             add_granularity=0):
448         ''' set the date to the value in spec
449         '''
450         self.year = self.month = self.week = self.day = self.hour = \
451             self.minute = self.second = 0
452         self.sign = 1
453         m = serialised_re.match(spec)
454         if not m:
455             m = interval_re.match(spec)
456             if not m:
457                 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
458                     '[#d] [[[H]H:MM]:SS] [date spec]')
459         else:
460             allowdate = 0
462         # pull out all the info specified
463         info = m.groupdict()
464         if add_granularity:
465             _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
467         valid = 0
468         for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
469                 'H':'hour', 'M':'minute', 'S':'second'}.items():
470             if info.get(group, None) is not None:
471                 valid = 1
472                 setattr(self, attr, int(info[group]))
474         # make sure it's valid
475         if not valid and not info['D']:
476             raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
477                 '[#d] [[[H]H:MM]:SS]')
479         if self.week:
480             self.day = self.day + self.week*7
482         if info['s'] is not None:
483             self.sign = {'+':1, '-':-1}[info['s']]
485         # use a date spec if one is given
486         if allowdate and info['D'] is not None:
487             now = Date('.')
488             date = Date(info['D'])
489             # if no time part was specified, nuke it in the "now" date
490             if not date.hour or date.minute or date.second:
491                 now.hour = now.minute = now.second = 0
492             if date != now:
493                 y = now - (date + self)
494                 self.__init__(y.get_tuple())
496     def __cmp__(self, other):
497         """Compare this interval to another interval."""
498         if other is None:
499             # we are always larger than None
500             return 1
501         for attr in 'sign year month day hour minute second'.split():
502             r = cmp(getattr(self, attr), getattr(other, attr))
503             if r:
504                 return r
505         return 0
507     def __str__(self):
508         """Return this interval as a string."""
509         l = []
510         if self.year: l.append('%sy'%self.year)
511         if self.month: l.append('%sm'%self.month)
512         if self.day: l.append('%sd'%self.day)
513         if self.second:
514             l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
515         elif self.hour or self.minute:
516             l.append('%d:%02d'%(self.hour, self.minute))
517         if l:
518             l.insert(0, {1:'+', -1:'-'}[self.sign])
519         return ' '.join(l)
521     def __add__(self, other):
522         if isinstance(other, Date):
523             # the other is a Date - produce a Date
524             return Date(other.addInterval(self))
525         elif isinstance(other, Interval):
526             # add the other Interval to this one
527             a = self.get_tuple()
528             as = a[0]
529             b = other.get_tuple()
530             bs = b[0]
531             i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
532             i.insert(0, 1)
533             i = fixTimeOverflow(i)
534             return Interval(i)
535         # nope, no idea what to do with this other...
536         raise TypeError, "Can't add %r"%other
538     def __sub__(self, other):
539         if isinstance(other, Date):
540             # the other is a Date - produce a Date
541             interval = Interval(self.get_tuple())
542             interval.sign *= -1
543             return Date(other.addInterval(interval))
544         elif isinstance(other, Interval):
545             # add the other Interval to this one
546             a = self.get_tuple()
547             as = a[0]
548             b = other.get_tuple()
549             bs = b[0]
550             i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
551             i.insert(0, 1)
552             i = fixTimeOverflow(i)
553             return Interval(i)
554         # nope, no idea what to do with this other...
555         raise TypeError, "Can't add %r"%other
557     def __div__(self, other):
558         """ Divide this interval by an int value.
560             Can't divide years and months sensibly in the _same_
561             calculation as days/time, so raise an error in that situation.
562         """
563         try:
564             other = float(other)
565         except TypeError:
566             raise ValueError, "Can only divide Intervals by numbers"
568         y, m, d, H, M, S = (self.year, self.month, self.day,
569             self.hour, self.minute, self.second)
570         if y or m:
571             if d or H or M or S:
572                 raise ValueError, "Can't divide Interval with date and time"
573             months = self.year*12 + self.month
574             months *= self.sign
576             months = int(months/other)
578             sign = months<0 and -1 or 1
579             m = months%12
580             y = months / 12
581             return Interval((sign, y, m, 0, 0, 0, 0))
583         else:
584             # handle a day/time division
585             seconds = S + M*60 + H*60*60 + d*60*60*24
586             seconds *= self.sign
588             seconds = int(seconds/other)
590             sign = seconds<0 and -1 or 1
591             seconds *= sign
592             S = seconds%60
593             seconds /= 60
594             M = seconds%60
595             seconds /= 60
596             H = seconds%24
597             d = seconds / 24
598             return Interval((sign, 0, 0, d, H, M, S))
600     def __repr__(self):
601         return '<Interval %s>'%self.__str__()
603     def pretty(self):
604         ''' print up the date date using one of these nice formats..
605         '''
606         if self.year:
607             if self.year == 1:
608                 s = _('1 year')
609             else:
610                 s = _('%(number)s years')%{'number': self.year}
611         elif self.month or self.day > 13:
612             days = (self.month * 30) + self.day
613             if days > 28:
614                 if int(days/30) > 1:
615                     s = _('%(number)s months')%{'number': int(days/30)}
616                 else:
617                     s = _('1 month')
618             else:
619                 s = _('%(number)s weeks')%{'number': int(days/7)}
620         elif self.day > 7:
621             s = _('1 week')
622         elif self.day > 1:
623             s = _('%(number)s days')%{'number': self.day}
624         elif self.day == 1 or self.hour > 12:
625             if self.sign > 0:
626                 return _('tomorrow')
627             else:
628                 return _('yesterday')
629         elif self.hour > 1:
630             s = _('%(number)s hours')%{'number': self.hour}
631         elif self.hour == 1:
632             if self.minute < 15:
633                 s = _('an hour')
634             elif self.minute/15 == 2:
635                 s = _('1 1/2 hours')
636             else:
637                 s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
638         elif self.minute < 1:
639             if self.sign > 0:
640                 return _('in a moment')
641             else:
642                 return _('just now')
643         elif self.minute == 1:
644             s = _('1 minute')
645         elif self.minute < 15:
646             s = _('%(number)s minutes')%{'number': self.minute}
647         elif int(self.minute/15) == 2:
648             s = _('1/2 an hour')
649         else:
650             s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
651         if self.sign < 0: 
652             s = s + _(' ago')
653         else:
654             s = _('in ') + s
655         return s
657     def get_tuple(self):
658         return (self.sign, self.year, self.month, self.day, self.hour,
659             self.minute, self.second)
661     def serialise(self):
662         sign = self.sign > 0 and '+' or '-'
663         return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
664             self.day, self.hour, self.minute, self.second)
666 def fixTimeOverflow(time):
667     """ Handle the overflow in the time portion (H, M, S) of "time":
668             (sign, y,m,d,H,M,S)
670         Overflow and underflow will at most affect the _days_ portion of
671         the date. We do not overflow days to months as we don't know _how_
672         to, generally.
673     """
674     # XXX we could conceivably use this function for handling regular dates
675     # XXX too - we just need to interrogate the month/year for the day
676     # XXX overflow...
678     sign, y, m, d, H, M, S = time
679     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
680     if seconds:
681         sign = seconds<0 and -1 or 1
682         seconds *= sign
683         S = seconds%60
684         seconds /= 60
685         M = seconds%60
686         seconds /= 60
687         H = seconds%24
688         d = seconds / 24
689     else:
690         months = y*12 + m
691         sign = months<0 and -1 or 1
692         months *= sign
693         m = months%12
694         y = months/12
696     return (sign, y, m, d, H, M, S)
698 class Range:
699     """Represents range between two values
700     Ranges can be created using one of theese two alternative syntaxes:
701         
702     1. Native english syntax::
704             [[From] <value>][ To <value>]
706        Keywords "From" and "To" are case insensitive. Keyword "From" is
707        optional.
709     2. "Geek" syntax::
711           [<value>][; <value>]
713     Either first or second <value> can be omitted in both syntaxes.
715     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003)::
717         >>> Range("from 2-12 to 4-2")
718         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
719         
720         >>> Range("18:00 TO +2m")
721         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
722         
723         >>> Range("12:00")
724         <Range from 2003-03-08.12:00:00 to None>
725         
726         >>> Range("tO +3d")
727         <Range from None to 2003-03-11.20:07:48>
728         
729         >>> Range("2002-11-10; 2002-12-12")
730         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
731         
732         >>> Range("; 20:00 +1d")
733         <Range from None to 2003-03-09.20:00:00>
735     """
736     def __init__(self, spec, Type, allow_granularity=1, **params):
737         """Initializes Range of type <Type> from given <spec> string.
738         
739         Sets two properties - from_value and to_value. None assigned to any of
740         this properties means "infinitum" (-infinitum to from_value and
741         +infinitum to to_value)
743         The Type parameter here should be class itself (e.g. Date), not a
744         class instance.
745         
746         """
747         self.range_type = Type
748         re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
749         re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
750         # Check which syntax to use
751         if  spec.find(';') == -1:
752             # Native english
753             mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
754         else:
755             # Geek
756             mch_range = re.search(re_geek_range, spec.strip())
757         if mch_range:
758             self.from_value, self.to_value = mch_range.groups()
759             if self.from_value:
760                 self.from_value = Type(self.from_value.strip(), **params)
761             if self.to_value:
762                 self.to_value = Type(self.to_value.strip(), **params)
763         else:
764             if allow_granularity:
765                 self.from_value = Type(spec, **params)
766                 self.to_value = Type(spec, add_granularity=1, **params)
767             else:
768                 raise ValueError, "Invalid range"
770     def __str__(self):
771         return "from %s to %s" % (self.from_value, self.to_value)
773     def __repr__(self):
774         return "<Range %s>" % self.__str__()
775  
776 def test_range():
777     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
778         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
779     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
780     for rspec in rspecs:
781         print '>>> Range("%s")' % rspec
782         print `Range(rspec, Date)`
783         print
784     for rspec in rispecs:
785         print '>>> Range("%s")' % rspec
786         print `Range(rspec, Interval)`
787         print
789 def test():
790     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
791     for interval in intervals:
792         print '>>> Interval("%s")'%interval
793         print `Interval(interval)`
795     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
796         "08-13.22:13", "14:25", '2002-12')
797     for date in dates:
798         print '>>> Date("%s")'%date
799         print `Date(date)`
801     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
802     for date, interval in sums:
803         print '>>> Date("%s") + Interval("%s")'%(date, interval)
804         print `Date(date) + Interval(interval)`
806 if __name__ == '__main__':
807     test()
809 # vim: set filetype=python ts=4 sw=4 et si