diff --git a/roundup/date.py b/roundup/date.py
index 484889bbeac2bda60d85075482811a18abeeedcb..3e6559ff0bad626efeee0264823ee649d33b00ef 100644 (file)
--- a/roundup/date.py
+++ b/roundup/date.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: date.py,v 1.27 2002-09-10 01:27:13 richard Exp $
+# $Id: date.py,v 1.46 2003-03-08 20:41:45 kedder Exp $
__doc__ = """
Date, time and time interval handling.
"""
-import time, re, calendar
+import time, re, calendar, types
from i18n import _
class Date:
"""
return Date(self.addInterval(interval))
- # XXX deviates from spec to allow subtraction of dates as well
+ # deviates from spec to allow subtraction of dates as well
def __sub__(self, other):
""" Subtract:
1. an interval from this date to produce another date.
return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
self.hour, self.minute, self.second)
- def pretty(self):
+ def pretty(self, format='%d %B %Y'):
''' print up the date date using a pretty format...
+
+ Note that if the day is zero, and the day appears first in the
+ format, then the day number will be removed from output.
'''
- str = time.strftime('%d %B %Y', (self.year, self.month,
- self.day, self.hour, self.minute, self.second, 0, 0, 0))
- if str[0] == '0': return ' ' + str[1:]
+ str = time.strftime(format, (self.year, self.month, self.day,
+ self.hour, self.minute, self.second, 0, 0, 0))
+ # handle zero day by removing it
+ if format.startswith('%d') and str[0] == '0':
+ return ' ' + str[1:]
return str
def set(self, spec, offset=0, date_re=re.compile(r'''
(((?P<y>\d\d\d\d)-)?((?P<m>\d\d?)-(?P<d>\d\d?))?)? # yyyy-mm-dd
- (?P<n>\.)? # .
- (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # hh:mm:ss
- (?P<o>.+)? # offset
- ''', re.VERBOSE), serialised_re=re.compile('''
- (?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2}) # yyyymmdd
- (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2}) # HHMMSS
+ (?P<n>\.)? # .
+ (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # hh:mm:ss
+ (?P<o>.+)? # offset
+ ''', re.VERBOSE), serialised_re=re.compile(r'''
+ (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
''', re.VERBOSE)):
''' set the date to the value in spec
'''
m = serialised_re.match(spec)
- if not m:
- m = date_re.match(spec)
- if not m:
- raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
- '[[h]h:mm[:ss]][offset]')
+ if m is not None:
+ # we're serialised - easy!
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second = map(int, m.groups()[:6])
+ return
+
+ # not serialised data, try usual format
+ m = date_re.match(spec)
+ if m is None:
+ raise ValueError, _('Not a date spec: [[yyyy-]mm-dd].'
+ '[[h]h:mm[:ss]][offset]')
info = m.groupdict()
- # get the current date/time using the offset
+ # get the current date as our default
y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
# override year, month, day parts
if info['m'] is not None and info['d'] is not None:
m = int(info['m'])
d = int(info['d'])
- if info['y'] is not None: y = int(info['y'])
- H = M = S = 0
+ if info['y'] is not None:
+ y = int(info['y'])
+ # time defaults to 00:00:00 GMT - offset (local midnight)
+ H = -offset
+ M = S = 0
# override hour, minute, second parts
if info['H'] is not None and info['M'] is not None:
return '<Date %s>'%self.__str__()
def local(self, offset):
- """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
- t = (self.year, self.month, self.day, self.hour + offset, self.minute,
- self.second, 0, 0, 0)
- self.year, self.month, self.day, self.hour, self.minute, \
- self.second, x, x, x = time.gmtime(calendar.timegm(t))
+ """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
+ """
+ return Date((self.year, self.month, self.day, self.hour + offset,
+ self.minute, self.second, 0, 0, 0))
def get_tuple(self):
return (self.year, self.month, self.day, self.hour, self.minute,
Example usage:
>>> Interval(" 3w 1 d 2:00")
- <Interval 22d 2:00>
+ <Interval + 22d 2:00>
>>> Date(". + 2d") + Interval("- 3w")
<Date 2000-06-07.00:34:02>
-
- Intervals are added/subtracted in order of:
+ >>> Interval('1:59:59') + Interval('00:00:01')
+ <Interval + 2:00>
+ >>> Interval('2:00') + Interval('- 00:00:01')
+ <Interval + 1:59:59>
+ >>> Interval('1y')/2
+ <Interval + 6m>
+ >>> Interval('1:00')/2
+ <Interval + 0:30>
+
+ Interval arithmetic is handled in a couple of special ways, trying
+ to cater for the most common cases. Fundamentally, Intervals which
+ have both date and time parts will result in strange results in
+ arithmetic - because of the impossibility of handling day->month->year
+ over- and under-flows. Intervals may also be divided by some number.
+
+ Intervals are added to Dates in order of:
seconds, minutes, hours, years, months, days
- Calculations involving monts (eg '+2m') have no effect on days - only
+ Calculations involving months (eg '+2m') have no effect on days - only
days (or over/underflow from hours/mins/secs) will do that, and
days-per-month and leap years are accounted for. Leap seconds are not.
"""Compare this interval to another interval."""
if other is None:
return 1
- for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
+ for attr in 'sign year month day hour minute second'.split():
if not hasattr(other, attr):
return 1
r = cmp(getattr(self, attr), getattr(other, attr))
- if r: return r
+ if r:
+ return r
return 0
def __str__(self):
"""Return this interval as a string."""
- sign = {1:'+', -1:'-'}[self.sign]
- l = [sign]
+ l = []
if self.year: l.append('%sy'%self.year)
if self.month: l.append('%sm'%self.month)
if self.day: l.append('%sd'%self.day)
l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
elif self.hour or self.minute:
l.append('%d:%02d'%(self.hour, self.minute))
+ if l:
+ l.insert(0, {1:'+', -1:'-'}[self.sign])
return ' '.join(l)
+ def __add__(self, other):
+ if isinstance(other, Date):
+ # the other is a Date - produce a Date
+ return Date(other.addInterval(self))
+ elif isinstance(other, Interval):
+ # add the other Interval to this one
+ a = self.get_tuple()
+ as = a[0]
+ b = other.get_tuple()
+ bs = b[0]
+ i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
+ i.insert(0, 1)
+ i = fixTimeOverflow(i)
+ return Interval(i)
+ # nope, no idea what to do with this other...
+ raise TypeError, "Can't add %r"%other
+
+ def __sub__(self, other):
+ if isinstance(other, Date):
+ # the other is a Date - produce a Date
+ interval = Interval(self.get_tuple())
+ interval.sign *= -1
+ return Date(other.addInterval(interval))
+ elif isinstance(other, Interval):
+ # add the other Interval to this one
+ a = self.get_tuple()
+ as = a[0]
+ b = other.get_tuple()
+ bs = b[0]
+ i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
+ i.insert(0, 1)
+ i = fixTimeOverflow(i)
+ return Interval(i)
+ # nope, no idea what to do with this other...
+ raise TypeError, "Can't add %r"%other
+
+ def __div__(self, other):
+ ''' Divide this interval by an int value.
+
+ Can't divide years and months sensibly in the _same_
+ calculation as days/time, so raise an error in that situation.
+ '''
+ try:
+ other = float(other)
+ except TypeError:
+ raise ValueError, "Can only divide Intervals by numbers"
+
+ y, m, d, H, M, S = (self.year, self.month, self.day,
+ self.hour, self.minute, self.second)
+ if y or m:
+ if d or H or M or S:
+ raise ValueError, "Can't divide Interval with date and time"
+ months = self.year*12 + self.month
+ months *= self.sign
+
+ months = int(months/other)
+
+ sign = months<0 and -1 or 1
+ m = months%12
+ y = months / 12
+ return Interval((sign, y, m, 0, 0, 0, 0))
+
+ else:
+ # handle a day/time division
+ seconds = S + M*60 + H*60*60 + d*60*60*24
+ seconds *= self.sign
+
+ seconds = int(seconds/other)
+
+ sign = seconds<0 and -1 or 1
+ seconds *= sign
+ S = seconds%60
+ seconds /= 60
+ M = seconds%60
+ seconds /= 60
+ H = seconds%24
+ d = seconds / 24
+ return Interval((sign, 0, 0, d, H, M, S))
+
def set(self, spec, interval_re=re.compile('''
\s*(?P<s>[-+])? # + or -
\s*((?P<y>\d+\s*)y)? # year
\s*((?P<d>\d+\s*)d)? # day
\s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
\s*''', re.VERBOSE), serialised_re=re.compile('''
- (?P<s>[+-])(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})
+ (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
(?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE)):
''' set the date to the value in spec
'''
s = _('1/2 an hour')
else:
s = _('%(number)s/4 hour')%{'number': int(self.minute/15)}
+ if self.sign < 0:
+ s = s + _(' ago')
+ else:
+ s = _('in') + s
return s
def get_tuple(self):
self.minute, self.second)
def serialise(self):
- return '%s%4d%02d%02d%02d%02d%02d'%(self.sign, self.year, self.month,
+ sign = self.sign > 0 and '+' or '-'
+ return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
self.day, self.hour, self.minute, self.second)
+def fixTimeOverflow(time):
+ ''' Handle the overflow in the time portion (H, M, S) of "time":
+ (sign, y,m,d,H,M,S)
+
+ Overflow and underflow will at most affect the _days_ portion of
+ the date. We do not overflow days to months as we don't know _how_
+ to, generally.
+ '''
+ # XXX we could conceivably use this function for handling regular dates
+ # XXX too - we just need to interrogate the month/year for the day
+ # XXX overflow...
+
+ sign, y, m, d, H, M, S = time
+ seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
+ if seconds:
+ sign = seconds<0 and -1 or 1
+ seconds *= sign
+ S = seconds%60
+ seconds /= 60
+ M = seconds%60
+ seconds /= 60
+ H = seconds%24
+ d = seconds / 24
+ else:
+ months = y*12 + m
+ sign = months<0 and -1 or 1
+ months *= sign
+ m = months%12
+ y = months/12
+
+ return (sign, y, m, d, H, M, S)
+
+class Range:
+ """
+ Represents range between two values
+ Ranges can be created using one of theese two alternative syntaxes:
+
+ 1. Native english syntax:
+ [[From] <value>][ To <value>]
+ Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
+
+ 2. "Geek" syntax:
+ [<value>][; <value>]
+
+ Either first or second <value> can be omitted in both syntaxes.
+
+ Examples (consider local time is Sat Mar 8 22:07:48 EET 2003):
+ >>> Range("from 2-12 to 4-2")
+ <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
+
+ >>> Range("18:00 TO +2m")
+ <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
+
+ >>> Range("12:00")
+ <Range from 2003-03-08.12:00:00 to None>
+
+ >>> Range("tO +3d")
+ <Range from None to 2003-03-11.20:07:48>
+
+ >>> Range("2002-11-10; 2002-12-12")
+ <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
+
+ >>> Range("; 20:00 +1d")
+ <Range from None to 2003-03-09.20:00:00>
+
+ """
+ def __init__(self, spec, type, **params):
+ """Initializes Range of type <type> from given <spec> string.
+
+ Sets two properties - from_value and to_value. None assigned to any of
+ this properties means "infinitum" (-infinitum to from_value and
+ +infinitum to to_value)
+ """
+ self.range_type = type
+ re_range = r'(?:^|(?:from)?(.+?))(?:to(.+?)$|$)'
+ re_geek_range = r'(?:^|(.+?))(?:;(.+?)$|$)'
+ # Check which syntax to use
+ if spec.find(';') == -1:
+ # Native english
+ mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
+ else:
+ # Geek
+ mch_range = re.search(re_geek_range, spec.strip())
+ if mch_range:
+ self.from_value, self.to_value = mch_range.groups()
+ if self.from_value:
+ self.from_value = type(self.from_value.strip(), **params)
+ if self.to_value:
+ self.to_value = type(self.to_value.strip(), **params)
+ else:
+ raise ValueError, "Invalid range"
+
+ def __str__(self):
+ return "from %s to %s" % (self.from_value, self.to_value)
+
+ def __repr__(self):
+ return "<Range %s>" % self.__str__()
+
+def test_range():
+ rspecs = ("from 2-12 to 4-2", "18:00 TO +2m", "12:00", "tO +3d",
+ "2002-11-10; 2002-12-12", "; 20:00 +1d")
+ for rspec in rspecs:
+ print '>>> Range("%s")' % rspec
+ print `Range(rspec, Date)`
+ print
def test():
intervals = (" 3w 1 d 2:00", " + 2d", "3w")
print `Date(date) + Interval(interval)`
if __name__ == '__main__':
- test()
+ test_range()
# vim: set filetype=python ts=4 sw=4 et si