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.
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.
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.
18 # $Id: date.py,v 1.94 2007-12-23 00:23:23 richard Exp $
20 """Date, time and time interval handling.
22 __docformat__
= 'restructuredtext'
34 from roundup
import i18n
36 # no, I don't know why we must anchor the date RE when we only ever use it
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
42 (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d?(\.\d+)?))?)? # hh:mm:ss
43 (?P<o>[\d\smywd\-+]+)? # offset
45 serialised_date_re
= re
.compile(r
'''
46 (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d?(\.\d+)?)
49 _timedelta0
= datetime
.timedelta(0)
55 # fallback implementation from Python Library Reference
57 class _UTC(datetime
.tzinfo
):
59 """Universal Coordinated Time zoneinfo"""
61 def utcoffset(self
, dt
):
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
)
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__()
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
):
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?
128 # try integer offset first for backward compatibility
131 except (TypeError, ValueError):
137 return SimpleTimezone(utcoffset
)
138 # tz is a timezone name
140 return pytz
.timezone(tz
)
143 elif tz
in _tzoffsets
:
144 return SimpleTimezone(_tzoffsets
[tz
], tz
)
148 def _utc_to_local(y
,m
,d
,H
,M
,S
,tz
):
149 TZ
= get_timezone(tz
)
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]
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]
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.
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::
202 <Date 2000-06-26.00:34:02>
204 "2000-06-25.19:34:02"
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::
224 <Date 2004-04-06.22:04:20.766830>
225 >>> d2=Date('2003-07-01')
227 <Date 2003-07-01.00:00:0.000000>
229 <Interval + 280d 22:04:20>
232 <Date 2004-04-06.22:04:20.000000>
234 <Date 2003-07-01.00:00:0.000000>
237 def __init__(self
, spec
='.', offset
=0, add_granularity
=False,
239 """Construct a date given a specification and a time zone offset.
242 is a full date or a partial form, with an optional added or
243 subtracted interval. Or a date 9-tuple.
245 is the local time zone offset from GMT in hours.
247 is i18n module or one of gettext translation classes.
248 It must have attributes 'gettext' and 'ngettext',
249 serving as translation functions.
251 self
.setTranslator(translator
)
252 if type(spec
) == type(''):
253 self
.set(spec
, offset
=offset
, add_granularity
=add_granularity
)
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'):
262 elif isinstance(spec
, Date
):
263 spec
= spec
.get_tuple()
265 y
,m
,d
,H
,M
,S
,x
,x
,x
= spec
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
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
280 m
= serialised_re
.match(spec
)
282 # we're serialised - easy!
284 (self
.year
, self
.month
, self
.day
, self
.hour
, self
.minute
) = \
286 self
.second
= float(g
[5])
289 # not serialised data, try usual format
290 m
= date_re
.match(spec
)
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"')
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".
303 for gran
in 'SMHdmy':
304 if info
[gran
] is not None:
308 add_granularity
= Interval('00:01')
310 add_granularity
= Interval('01:00')
312 add_granularity
= Interval('+1%s'%gran
)
315 raise ValueError(self
._('Could not determine granularity'))
317 # get the current date as our default
318 dt
= datetime
.datetime
.utcnow()
319 y
,m
,d
,H
,M
,S
,x
,x
,x
= dt
.timetuple()
320 S
+= dt
.microsecond
/1000000.
322 # whether we need to convert to UTC
325 if info
['y'] is not None or info
['a'] is not None:
326 if info
['y'] is not None:
329 if info
['m'] is not None:
331 if info
['d'] is not None:
333 if info
['a'] is not None:
340 # override hour, minute, second parts
341 if info
['H'] is not None and info
['M'] is not None:
345 if info
['S'] is not None:
350 # now handle the adjustment of hour
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()
355 y
, m
, d
, H
, M
, S
= _local_to_utc(y
, m
, d
, H
, M
, S
, offset
)
356 self
.year
, self
.month
, self
.day
, self
.hour
, self
.minute
, \
357 self
.second
= y
, m
, d
, H
, M
, S
358 # we lost the fractional part along the way
359 self
.second
+= dt
.microsecond
/1000000.
361 if info
.get('o', None):
363 self
.applyInterval(Interval(info
['o'], allowdate
=0))
365 raise ValueError, self
._('%r not a date / time spec '
366 '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
367 '"yyyy-mm-dd.HH:MM:SS.SSS"')%(spec
,)
369 # adjust by added granularity
371 self
.applyInterval(add_granularity
)
372 self
.applyInterval(Interval('- 00:00:01'))
374 def addInterval(self
, interval
):
375 ''' Add the interval to this date, returning the date tuple
379 year
= self
.year
+ sign
* interval
.year
380 month
= self
.month
+ sign
* interval
.month
381 day
= self
.day
+ sign
* interval
.day
382 hour
= self
.hour
+ sign
* interval
.hour
383 minute
= self
.minute
+ sign
* interval
.minute
384 # Intervals work on whole seconds
385 second
= int(self
.second
) + sign
* interval
.second
387 # now cope with under- and over-flow
389 while (second
< 0 or second
> 59 or minute
< 0 or minute
> 59 or
390 hour
< 0 or hour
> 23):
391 if second
< 0: minute
-= 1; second
+= 60
392 elif second
> 59: minute
+= 1; second
-= 60
393 if minute
< 0: hour
-= 1; minute
+= 60
394 elif minute
> 59: hour
+= 1; minute
-= 60
395 if hour
< 0: day
-= 1; hour
+= 24
396 elif hour
> 23: day
+= 1; hour
-= 24
398 # fix up the month so we're within range
399 while month
< 1 or month
> 12:
400 if month
< 1: year
-= 1; month
+= 12
401 if month
> 12: year
+= 1; month
-= 12
403 # now do the days, now that we know what month we're in
404 def get_mdays(year
, month
):
405 if month
== 2 and calendar
.isleap(year
): return 29
406 else: return calendar
.mdays
[month
]
408 while month
< 1 or month
> 12 or day
< 1 or day
> get_mdays(year
,month
):
409 # now to day under/over
411 # When going backwards, decrement month, then increment days
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
)
419 # possibly fix up the month so we're within range
420 while month
< 1 or month
> 12:
421 if month
< 1: year
-= 1; month
+= 12 ; day
+= 31
422 if month
> 12: year
+= 1; month
-= 12
424 return (year
, month
, day
, hour
, minute
, second
, 0, 0, 0)
426 def differenceDate(self
, other
):
427 "Return the difference between this date and another date"
430 def applyInterval(self
, interval
):
431 ''' Apply the interval to this date
433 self
.year
, self
.month
, self
.day
, self
.hour
, self
.minute
, \
434 self
.second
, x
, x
, x
= self
.addInterval(interval
)
436 def __add__(self
, interval
):
437 """Add an interval to this date to produce another date.
439 return Date(self
.addInterval(interval
), translator
=self
.translator
)
441 # deviates from spec to allow subtraction of dates as well
442 def __sub__(self
, other
):
444 1. an interval from this date to produce another date.
445 2. a date from this date to produce an interval.
447 if isinstance(other
, Interval
):
448 other
= Interval(other
.get_tuple())
450 return self
.__add__(other
)
452 assert isinstance(other
, Date
), 'May only subtract Dates or Intervals'
454 return self
.dateDelta(other
)
456 def dateDelta(self
, other
):
457 """ Produce an Interval of the difference between this date
458 and another date. Only returns days:hours:minutes:seconds.
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
475 H
= (diff
/(60*60))%24
477 return Interval((0, 0, d
, H
, M
, S
), sign
=sign
,
478 translator
=self
.translator
)
480 def __cmp__(self
, other
, int_seconds
=0):
481 """Compare this date to another date."""
484 for attr
in ('year', 'month', 'day', 'hour', 'minute'):
485 if not hasattr(other
, attr
):
487 r
= cmp(getattr(self
, attr
), getattr(other
, attr
))
489 if not hasattr(other
, 'second'):
492 return cmp(int(self
.second
), int(other
.second
))
493 return cmp(self
.second
, other
.second
)
496 """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
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
,
504 def pretty(self
, format
='%d %B %Y'):
505 ''' print up the date date using a pretty format...
507 Note that if the day is zero, and the day appears first in the
508 format, then the day number will be removed from output.
510 dt
= datetime
.datetime(self
.year
, self
.month
, self
.day
, self
.hour
,
511 self
.minute
, int(self
.second
),
512 int ((self
.second
- int (self
.second
)) * 1000000.))
513 str = dt
.strftime(format
)
515 # handle zero day by removing it
516 if format
.startswith('%d') and str[0] == '0':
521 return '<Date %s>'%self
.formal(sec
='%06.3f')
523 def local(self
, offset
):
524 """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
526 y
, m
, d
, H
, M
, S
= _utc_to_local(self
.year
, self
.month
, self
.day
,
527 self
.hour
, self
.minute
, self
.second
, offset
)
528 return Date((y
, m
, d
, H
, M
, S
, 0, 0, 0), translator
=self
.translator
)
530 def __deepcopy__(self
, memo
):
531 return Date((self
.year
, self
.month
, self
.day
, self
.hour
,
532 self
.minute
, self
.second
, 0, 0, 0), translator
=self
.translator
)
535 return (self
.year
, self
.month
, self
.day
, self
.hour
, self
.minute
,
536 self
.second
, 0, 0, 0)
539 return '%04d%02d%02d%02d%02d%06.3f'%(self
.year
, self
.month
,
540 self
.day
, self
.hour
, self
.minute
, self
.second
)
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
550 def setTranslator(self
, translator
):
551 """Replace the translation engine
554 is i18n module or one of gettext translation classes.
555 It must have attributes 'gettext' and 'ngettext',
556 serving as translation functions.
558 self
.translator
= translator
559 self
._
= translator
.gettext
560 self
.ngettext
= translator
.ngettext
562 def fromtimestamp(cls
, ts
):
563 """Create a date object from a timestamp.
565 The timestamp may be outside the gmtime year-range of
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
)
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
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
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')
595 >>> Interval('2:00') + Interval('- 00:00:01')
599 >>> Interval('1:00')/2
601 >>> Interval('2003-03-18')
602 <Interval + [number of days between now and 2003-03-18]>
603 >>> Interval('-4d 2003-03-18')
604 <Interval + [number of days between now and 2003-03-14]>
606 Interval arithmetic is handled in a couple of special ways, trying
607 to cater for the most common cases. Fundamentally, Intervals which
608 have both date and time parts will result in strange results in
609 arithmetic - because of the impossibility of handling day->month->year
610 over- and under-flows. Intervals may also be divided by some number.
612 Intervals are added to Dates in order of:
613 seconds, minutes, hours, years, months, days
615 Calculations involving months (eg '+2m') have no effect on days - only
616 days (or over/underflow from hours/mins/secs) will do that, and
617 days-per-month and leap years are accounted for. Leap seconds are not.
619 The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
620 minute, second) is the serialisation format returned by the serialise()
621 method, and is accepted as an argument on instatiation.
623 TODO: more examples, showing the order of addition operation
625 def __init__(self
, spec
, sign
=1, allowdate
=1, add_granularity
=False,
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()
639 self
.sign
, self
.year
, self
.month
, self
.day
, self
.hour
, \
640 self
.minute
, self
.second
= spec
641 self
.second
= int(self
.second
)
643 # old, buggy spec form
645 self
.year
, self
.month
, self
.day
, self
.hour
, self
.minute
, \
647 self
.second
= int(self
.second
)
649 def __deepcopy__(self
, memo
):
650 return Interval((self
.sign
, self
.year
, self
.month
, self
.day
,
651 self
.hour
, self
.minute
, self
.second
), translator
=self
.translator
)
653 def set(self
, spec
, allowdate
=1, interval_re
=re
.compile('''
654 \s*(?P<s>[-+])? # + or -
655 \s*((?P<y>\d+\s*)y)? # year
656 \s*((?P<m>\d+\s*)m)? # month
657 \s*((?P<w>\d+\s*)w)? # week
658 \s*((?P<d>\d+\s*)d)? # day
659 \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
661 (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
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
670 self
.year
= self
.month
= self
.week
= self
.day
= self
.hour
= \
671 self
.minute
= self
.second
= 0
673 m
= serialised_re
.match(spec
)
675 m
= interval_re
.match(spec
)
677 raise ValueError, self
._('Not an interval spec:'
678 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
682 # pull out all the info specified
685 for gran
in 'SMHdwmy':
686 if info
[gran
] is not None:
687 info
[gran
] = int(info
[gran
]) + (info
['s']=='-' and -1 or 1)
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:
695 setattr(self
, attr
, int(info
[group
]))
697 # make sure it's valid
698 if not valid
and not info
['D']:
699 raise ValueError, self
._('Not an interval spec:'
700 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
703 self
.day
= self
.day
+ self
.week
*7
705 if info
['s'] is not None:
706 self
.sign
= {'+':1, '-':-1}[info
['s']]
708 # use a date spec if one is given
709 if allowdate
and info
['D'] is not None:
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
716 y
= now
- (date
+ self
)
717 self
.__init__(y
.get_tuple())
719 def __cmp__(self
, other
):
720 """Compare this interval to another interval."""
722 # we are always larger than None
724 for attr
in 'sign year month day hour minute second'.split():
725 r
= cmp(getattr(self
, attr
), getattr(other
, attr
))
731 """Return this interval as a string."""
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
)
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
))
741 l
.insert(0, {1:'+', -1:'-'}[self
.sign
])
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
754 b
= other
.get_tuple()
756 i
= [asgn
*x
+ bsgn
*y
for x
,y
in zip(a
[1:],b
[1:])]
758 i
= fixTimeOverflow(i
)
759 return Interval(i
, translator
=self
.translator
)
760 # nope, no idea what to do with this other...
761 raise TypeError, "Can't add %r"%other
763 def __sub__(self
, other
):
764 if isinstance(other
, Date
):
765 # the other is a Date - produce a Date
766 interval
= Interval(self
.get_tuple())
768 return Date(other
.addInterval(interval
),
769 translator
=self
.translator
)
770 elif isinstance(other
, Interval
):
771 # add the other Interval to this one
774 b
= other
.get_tuple()
776 i
= [asgn
*x
- bsgn
*y
for x
,y
in zip(a
[1:],b
[1:])]
778 i
= fixTimeOverflow(i
)
779 return Interval(i
, translator
=self
.translator
)
780 # nope, no idea what to do with this other...
781 raise TypeError, "Can't add %r"%other
783 def __div__(self
, other
):
784 """ Divide this interval by an int value.
786 Can't divide years and months sensibly in the _same_
787 calculation as days/time, so raise an error in that situation.
792 raise ValueError, "Can only divide Intervals by numbers"
794 y
, m
, d
, H
, M
, S
= (self
.year
, self
.month
, self
.day
,
795 self
.hour
, self
.minute
, self
.second
)
798 raise ValueError, "Can't divide Interval with date and time"
799 months
= self
.year
*12 + self
.month
802 months
= int(months
/other
)
804 sign
= months
<0 and -1 or 1
807 return Interval((sign
, y
, m
, 0, 0, 0, 0),
808 translator
=self
.translator
)
811 # handle a day/time division
812 seconds
= S
+ M
*60 + H
*60*60 + d
*60*60*24
815 seconds
= int(seconds
/other
)
817 sign
= seconds
<0 and -1 or 1
825 return Interval((sign
, 0, 0, d
, H
, M
, S
),
826 translator
=self
.translator
)
829 return '<Interval %s>'%self
.__str__()
832 ''' print up the date date using one of these nice formats..
834 _quarters
= self
.minute
/ 15
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
}
843 _weeks
= int(self
.day
/ 7)
844 s
= self
.ngettext("%(number)s week", "%(number)s weeks",
845 _weeks
) % {'number': _weeks
}
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:
852 return self
._('tomorrow')
854 return self
._('yesterday')
856 # Note: singular form is not used
857 s
= self
.ngettext('%(number)s hour', '%(number)s hours',
858 self
.hour
) % {'number': self
.hour
}
861 s
= self
._('an hour')
863 s
= self
._('1 1/2 hours')
865 s
= self
.ngettext('1 %(number)s/4 hours',
866 '1 %(number)s/4 hours', _quarters
)%{'number': _quarters
}
867 elif self
.minute
< 1:
869 return self
._('in a moment')
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
}
880 s
= self
._('1/2 an hour')
882 s
= self
.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
883 _quarters
) % {'number': _quarters
}
884 # XXX this is internationally broken
886 s
= self
._('%s ago') % s
888 s
= self
._('in %s') % s
892 return (self
.sign
, self
.year
, self
.month
, self
.day
, self
.hour
,
893 self
.minute
, self
.second
)
896 sign
= self
.sign
> 0 and '+' or '-'
897 return '%s%04d%02d%02d%02d%02d%02d'%(sign
, self
.year
, self
.month
,
898 self
.day
, self
.hour
, self
.minute
, self
.second
)
900 def as_seconds(self
):
901 '''Calculate the Interval as a number of seconds.
903 Months are counted as 30 days, years as 365 days. Returns a Long
907 n
= n
+ self
.month
* 30
917 def from_seconds(self
, val
):
918 '''Figure my second, minute, hour and day values using a seconds
927 self
.second
= val
% 60
929 self
.minute
= val
% 60
934 self
.month
= self
.year
= 0
936 def setTranslator(self
, translator
):
937 """Replace the translation engine
940 is i18n module or one of gettext translation classes.
941 It must have attributes 'gettext' and 'ngettext',
942 serving as translation functions.
944 self
.translator
= translator
945 self
._
= translator
.gettext
946 self
.ngettext
= translator
.ngettext
949 def fixTimeOverflow(time
):
950 """ Handle the overflow in the time portion (H, M, S) of "time":
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_
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
961 sign
, y
, m
, d
, H
, M
, S
= time
962 seconds
= sign
* (S
+ M
*60 + H
*60*60 + d
*60*60*24)
964 sign
= seconds
<0 and -1 or 1
974 sign
= months
<0 and -1 or 1
979 return (sign
, y
, m
, d
, H
, M
, S
)
982 """Represents range between two values
983 Ranges can be created using one of theese two alternative syntaxes:
985 1. Native english syntax::
987 [[From] <value>][ To <value>]
989 Keywords "From" and "To" are case insensitive. Keyword "From" is
996 Either first or second <value> can be omitted in both syntaxes.
998 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
1000 >>> Range("from 2-12 to 4-2")
1001 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
1003 >>> Range("18:00 TO +2m")
1004 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
1007 <Range from 2003-03-08.12:00:00 to None>
1010 <Range from None to 2003-03-11.20:07:48>
1012 >>> Range("2002-11-10; 2002-12-12")
1013 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
1015 >>> Range("; 20:00 +1d")
1016 <Range from None to 2003-03-09.20:00:00>
1019 def __init__(self
, spec
, Type
, allow_granularity
=True, **params
):
1020 """Initializes Range of type <Type> from given <spec> string.
1022 Sets two properties - from_value and to_value. None assigned to any of
1023 this properties means "infinitum" (-infinitum to from_value and
1024 +infinitum to to_value)
1026 The Type parameter here should be class itself (e.g. Date), not a
1029 self
.range_type
= Type
1030 re_range
= r
'(?:^|from(.+?))(?:to(.+?)$|$)'
1031 re_geek_range
= r
'(?:^|(.+?));(?:(.+?)$|$)'
1032 # Check which syntax to use
1035 m
= re
.search(re_geek_range
, spec
.strip())
1038 m
= re
.search(re_range
, spec
.strip(), re
.IGNORECASE
)
1040 self
.from_value
, self
.to_value
= m
.groups()
1042 self
.from_value
= Type(self
.from_value
.strip(), **params
)
1044 self
.to_value
= Type(self
.to_value
.strip(), **params
)
1046 if allow_granularity
:
1047 self
.from_value
= Type(spec
, **params
)
1048 self
.to_value
= Type(spec
, add_granularity
=True, **params
)
1050 raise ValueError, "Invalid range"
1053 return "from %s to %s" % (self
.from_value
, self
.to_value
)
1056 return "<Range %s>" % self
.__str__()
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
)`
1066 for rspec
in rispecs
:
1067 print '>>> Range("%s")' % rspec
1068 print `
Range(rspec
, Interval
)`
1072 intervals
= (" 3w 1 d 2:00", " + 2d", "3w")
1073 for interval
in intervals
:
1074 print '>>> Interval("%s")'%interval
1075 print `
Interval(interval
)`
1077 dates
= (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
1078 "08-13.22:13", "14:25", '2002-12')
1080 print '>>> Date("%s")'%date
1083 sums
= ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
1084 for date
, interval
in sums
:
1085 print '>>> Date("%s") + Interval("%s")'%(date
, interval
)
1086 print `
Date(date
) + Interval(interval
)`
1088 if __name__
== '__main__':
1091 # vim: set filetype=python sts=4 sw=4 et si :