diff --git a/roundup/date.py b/roundup/date.py
index d9ac75815bbceb8fbb93962b5698dbdd8366905a..365aa80b07cc670a641400ea896e0f9a753f8cd0 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.18 2002-01-23 20:00:50 jhermann Exp $
+# $Id: date.py,v 1.54 2003-04-23 11:48:05 richard Exp $
__doc__ = """
Date, time and time interval handling.
"""
-import time, re, calendar
+import time, re, calendar, types
from i18n import _
+def _add_granularity(src, order, value = 1):
+ '''Increment first non-None value in src dictionary ordered by 'order'
+ parameter
+ '''
+ for gran in order:
+ if src[gran]:
+ src[gran] = int(src[gran]) + value
+ break
+
class Date:
'''
As strings, date-and-time stamps are specified with the date in
"11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
"14:25" means <Date yyyy-mm-dd.19:25:00>
"8:47:11" means <Date yyyy-mm-dd.13:47:11>
+ "2003" means <Date 2003-01-01.00:00:00>
+ "2003-06" means <Date 2003-06-01.00:00:00>
"." means "right now"
The Date class should understand simple date expressions of the form
<Date 2000-08-14.03:13:00>
>>> Date("14:25", -5)
<Date 2000-06-25.19:25:00>
+
+ The date format 'yyyymmddHHMMSS' (year, month, day, hour,
+ minute, second) is the serialisation format returned by the serialise()
+ method, and is accepted as an argument on instatiation.
'''
- def __init__(self, spec='.', offset=0):
+
+ def __init__(self, spec='.', offset=0, add_granularity=0):
"""Construct a date given a specification and a time zone offset.
'spec' is a full date or a partial form, with an optional
'offset' is the local time zone offset from GMT in hours.
"""
if type(spec) == type(''):
- self.set(spec, offset=offset)
+ self.set(spec, offset=offset, add_granularity=add_granularity)
else:
y,m,d,H,M,S,x,x,x = spec
ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
self.year, self.month, self.day, self.hour, self.minute, \
self.second, x, x, x = time.gmtime(ts)
+ usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS][offset]'
+ 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<a>\d\d?)[/-](?P<b>\d\d?))? # or 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(r'''
+ (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)
+ ''', re.VERBOSE), add_granularity=0):
+ ''' set the date to the value in spec
+ '''
+
+ m = serialised_re.match(spec)
+ 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: %s' % self.usagespec)
+
+ info = m.groupdict()
+
+ if add_granularity:
+ _add_granularity(info, 'SMHdmyab')
+
+ # get the current date as our default
+ y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
+
+ if info['y'] is not None or info['a'] is not None:
+ if info['y'] is not None:
+ y = int(info['y'])
+ m,d = (1,1)
+ if info['m'] is not None:
+ m = int(info['m'])
+ if info['d'] is not None:
+ d = int(info['d'])
+ if info['a'] is not None:
+ m = int(info['a'])
+ d = int(info['b'])
+ H = -offset
+ M = S = 0
+
+ # override hour, minute, second parts
+ if info['H'] is not None and info['M'] is not None:
+ H = int(info['H']) - offset
+ M = int(info['M'])
+ S = 0
+ if info['S'] is not None: S = int(info['S'])
+
+ if add_granularity:
+ S = S - 1
+
+ # now handle the adjustment of hour
+ ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second, x, x, x = time.gmtime(ts)
+
+ if info.get('o', None):
+ try:
+ self.applyInterval(Interval(info['o'], allowdate=0))
+ except ValueError:
+ raise ValueError, _('Not a date spec: %s' % self.usagespec)
+
+ def addInterval(self, interval):
+ ''' Add the interval to this date, returning the date tuple
+ '''
+ # do the basic calc
+ sign = interval.sign
+ year = self.year + sign * interval.year
+ month = self.month + sign * interval.month
+ day = self.day + sign * interval.day
+ hour = self.hour + sign * interval.hour
+ minute = self.minute + sign * interval.minute
+ second = self.second + sign * interval.second
+
+ # now cope with under- and over-flow
+ # first do the time
+ while (second < 0 or second > 59 or minute < 0 or minute > 59 or
+ hour < 0 or hour > 59):
+ if second < 0: minute -= 1; second += 60
+ elif second > 59: minute += 1; second -= 60
+ if minute < 0: hour -= 1; minute += 60
+ elif minute > 59: hour += 1; minute -= 60
+ if hour < 0: day -= 1; hour += 24
+ elif hour > 59: day += 1; hour -= 24
+
+ # fix up the month so we're within range
+ while month < 1 or month > 12:
+ if month < 1: year -= 1; month += 12
+ if month > 12: year += 1; month -= 12
+
+ # now do the days, now that we know what month we're in
+ mdays = calendar.mdays
+ if month == 2 and calendar.isleap(year): month_days = 29
+ else: month_days = mdays[month]
+ while month < 1 or month > 12 or day < 0 or day > month_days:
+ # now to day under/over
+ if day < 0: month -= 1; day += month_days
+ elif day > month_days: month += 1; day -= month_days
+
+ # possibly fix up the month so we're within range
+ while month < 1 or month > 12:
+ if month < 1: year -= 1; month += 12
+ if month > 12: year += 1; month -= 12
+
+ # re-figure the number of days for this month
+ if month == 2 and calendar.isleap(year): month_days = 29
+ else: month_days = mdays[month]
+ return (year, month, day, hour, minute, second, 0, 0, 0)
+
def applyInterval(self, interval):
''' Apply the interval to this date
'''
- t = (self.year + interval.year,
- self.month + interval.month,
- self.day + interval.day,
- self.hour + interval.hour,
- self.minute + interval.minute,
- self.second + interval.second, 0, 0, 0)
self.year, self.month, self.day, self.hour, self.minute, \
- self.second, x, x, x = time.gmtime(calendar.timegm(t))
+ self.second, x, x, x = self.addInterval(interval)
- def __add__(self, other):
- """Add an interval to this date to produce another date."""
- t = (self.year + other.sign * other.year,
- self.month + other.sign * other.month,
- self.day + other.sign * other.day,
- self.hour + other.sign * other.hour,
- self.minute + other.sign * other.minute,
- self.second + other.sign * other.second, 0, 0, 0)
- return Date(time.gmtime(calendar.timegm(t)))
-
- # XXX deviates from spec to allow subtraction of dates as well
+ def __add__(self, interval):
+ """Add an interval to this date to produce another date.
+ """
+ return Date(self.addInterval(interval))
+
+ # 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.
2. a date from this date to produce an interval.
"""
- if isinstance(other, Date):
- # TODO this code will fall over laughing if the dates cross
- # leap years, phases of the moon, ....
- a = calendar.timegm((self.year, self.month, self.day, self.hour,
- self.minute, self.second, 0, 0, 0))
- b = calendar.timegm((other.year, other.month, other.day, other.hour,
- other.minute, other.second, 0, 0, 0))
- diff = a - b
- if diff < 0:
- sign = -1
- diff = -diff
- else:
- sign = 1
- S = diff%60
- M = (diff/60)%60
- H = (diff/(60*60))%60
- if H>1: S = 0
- d = (diff/(24*60*60))%30
- if d>1: H = S = M = 0
- m = (diff/(30*24*60*60))%12
- if m>1: H = S = M = 0
- y = (diff/(365*24*60*60))
- if y>1: d = H = S = M = 0
- return Interval((y, m, d, H, M, S), sign=sign)
- t = (self.year - other.sign * other.year,
- self.month - other.sign * other.month,
- self.day - other.sign * other.day,
- self.hour - other.sign * other.hour,
- self.minute - other.sign * other.minute,
- self.second - other.sign * other.second, 0, 0, 0)
- return Date(time.gmtime(calendar.timegm(t)))
+ if isinstance(other, Interval):
+ other = Interval(other.get_tuple())
+ other.sign *= -1
+ return self.__add__(other)
+
+ assert isinstance(other, Date), 'May only subtract Dates or Intervals'
+
+ # TODO this code will fall over laughing if the dates cross
+ # leap years, phases of the moon, ....
+ a = calendar.timegm((self.year, self.month, self.day, self.hour,
+ self.minute, self.second, 0, 0, 0))
+ b = calendar.timegm((other.year, other.month, other.day,
+ other.hour, other.minute, other.second, 0, 0, 0))
+ diff = a - b
+ if diff < 0:
+ sign = 1
+ diff = -diff
+ else:
+ sign = -1
+ S = diff%60
+ M = (diff/60)%60
+ H = (diff/(60*60))%60
+ if H>1: S = 0
+ d = (diff/(24*60*60))%30
+ if d>1: H = S = M = 0
+ m = (diff/(30*24*60*60))%12
+ if m>1: H = S = M = 0
+ y = (diff/(365*24*60*60))
+ if y>1: d = H = S = M = 0
+ return Interval((y, m, d, H, M, S), sign=sign)
def __cmp__(self, other):
"""Compare this date to another date."""
if other is None:
return 1
for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
+ if not hasattr(other, attr):
+ return 1
r = cmp(getattr(self, attr), getattr(other, attr))
if r: return r
return 0
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...
- '''
- 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:]
- 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)):
- ''' set the date to the value in spec
+ Note that if the day is zero, and the day appears first in the
+ format, then the day number will be removed from output.
'''
- m = date_re.match(spec)
- if not m:
- 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
- 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
-
- # override hour, minute, second parts
- if info['H'] is not None and info['M'] is not None:
- H = int(info['H']) - offset
- M = int(info['M'])
- S = 0
- if info['S'] is not None: S = int(info['S'])
-
- # now handle the adjustment of hour
- ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
- self.year, self.month, self.day, self.hour, self.minute, \
- self.second, x, x, x = time.gmtime(ts)
-
- if info['o']:
- self.applyInterval(Interval(info['o']))
+ 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 __repr__(self):
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,
self.second, 0, 0, 0)
+ def serialise(self):
+ return '%4d%02d%02d%02d%02d%02d'%(self.year, self.month,
+ self.day, self.hour, self.minute, self.second)
+
class Interval:
'''
Date intervals are specified using the suffixes "y", "m", and "d". The
Example usage:
>>> Interval(" 3w 1 d 2:00")
- <Interval 22d 2:00>
- >>> Date(". + 2d") - Interval("3w")
+ <Interval + 22d 2:00>
+ >>> Date(". + 2d") + Interval("- 3w")
<Date 2000-06-07.00:34:02>
+ >>> 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('2003-03-18')
+ <Interval + [number of days between now and 2003-03-18]>
+ >>> Interval('-4d 2003-03-18')
+ <Interval + [number of days between now and 2003-03-14]>
+
+ 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 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.
+
+ The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
+ minute, second) is the serialisation format returned by the serialise()
+ method, and is accepted as an argument on instatiation.
+
+ TODO: more examples, showing the order of addition operation
'''
- def __init__(self, spec, sign=1):
+ def __init__(self, spec, sign=1, allowdate=1, add_granularity=0):
"""Construct an interval given a specification."""
if type(spec) == type(''):
- self.set(spec)
+ self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
else:
- self.sign = sign
- self.year, self.month, self.day, self.hour, self.minute, \
- self.second = spec
+ if len(spec) == 7:
+ self.sign, self.year, self.month, self.day, self.hour, \
+ self.minute, self.second = spec
+ else:
+ # old, buggy spec form
+ self.sign = sign
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second = spec
+
+ def set(self, spec, allowdate=1, interval_re=re.compile('''
+ \s*(?P<s>[-+])? # + or -
+ \s*((?P<y>\d+\s*)y)? # year
+ \s*((?P<m>\d+\s*)m)? # month
+ \s*((?P<w>\d+\s*)w)? # week
+ \s*((?P<d>\d+\s*)d)? # day
+ \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
+ \s*(?P<D>
+ (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
+ \.? # .
+ (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
+ )?''', re.VERBOSE), serialised_re=re.compile('''
+ (?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),
+ add_granularity=0):
+ ''' set the date to the value in spec
+ '''
+ self.year = self.month = self.week = self.day = self.hour = \
+ self.minute = self.second = 0
+ self.sign = 1
+ m = serialised_re.match(spec)
+ if not m:
+ m = interval_re.match(spec)
+ if not m:
+ raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
+ '[#d] [[[H]H:MM]:SS] [date spec]')
+ else:
+ allowdate = 0
+
+ # pull out all the info specified
+ info = m.groupdict()
+ if add_granularity:
+ _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
+
+ valid = 0
+ for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
+ 'H':'hour', 'M':'minute', 'S':'second'}.items():
+ if info.get(group, None) is not None:
+ valid = 1
+ setattr(self, attr, int(info[group]))
+
+ # make sure it's valid
+ if not valid and not info['D']:
+ raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
+ '[#d] [[[H]H:MM]:SS]')
+
+ if self.week:
+ self.day = self.day + self.week*7
+
+ if info['s'] is not None:
+ self.sign = {'+':1, '-':-1}[info['s']]
+
+ # use a date spec if one is given
+ if allowdate and info['D'] is not None:
+ now = Date('.')
+ date = Date(info['D'])
+ # if no time part was specified, nuke it in the "now" date
+ if not date.hour or date.minute or date.second:
+ now.hour = now.minute = now.second = 0
+ if date != now:
+ y = now - (date + self)
+ self.__init__(y.get_tuple())
def __cmp__(self, other):
"""Compare this interval to another interval."""
if other is None:
+ # we are always larger than None
return 1
- for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
+ for attr in 'sign year month day hour minute second'.split():
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 set(self, spec, interval_re = re.compile('''
- \s*
- (?P<s>[-+])? # + or -
- \s*
- ((?P<y>\d+\s*)y)? # year
- \s*
- ((?P<m>\d+\s*)m)? # month
- \s*
- ((?P<w>\d+\s*)w)? # week
- \s*
- ((?P<d>\d+\s*)d)? # day
- \s*
- (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # time
- \s*
- ''', re.VERBOSE)):
- ''' set the date to the value in spec
+ 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.
'''
- self.year = self.month = self.week = self.day = self.hour = \
- self.minute = self.second = 0
- self.sign = 1
- m = interval_re.match(spec)
- if not m:
- raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] '
- '[#d] [[[H]H:MM]:SS]')
+ try:
+ other = float(other)
+ except TypeError:
+ raise ValueError, "Can only divide Intervals by numbers"
- info = m.groupdict()
- for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
- 'H':'hour', 'M':'minute', 'S':'second'}.items():
- if info[group] is not None:
- setattr(self, attr, int(info[group]))
+ 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
- if self.week:
- self.day = self.day + self.week*7
+ months = int(months/other)
- if info['s'] is not None:
- self.sign = {'+':1, '-':-1}[info['s']]
+ 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 __repr__(self):
return '<Interval %s>'%self.__str__()
def pretty(self):
''' print up the date date using one of these nice formats..
'''
- if self.year or self.month > 2:
- return None
- if self.month or self.day > 13:
+ if self.year:
+ if self.year == 1:
+ return _('1 year')
+ else:
+ return _('%(number)s years')%{'number': self.year}
+ elif self.month or self.day > 13:
days = (self.month * 30) + self.day
if days > 28:
if int(days/30) > 1:
- return _('%(number)s months')%{'number': int(days/30)}
+ s = _('%(number)s months')%{'number': int(days/30)}
else:
- return _('1 month')
+ s = _('1 month')
+ else:
+ s = _('%(number)s weeks')%{'number': int(days/7)}
+ elif self.day > 7:
+ s = _('1 week')
+ elif self.day > 1:
+ s = _('%(number)s days')%{'number': self.day}
+ elif self.day == 1 or self.hour > 12:
+ if self.sign > 0:
+ return _('tomorrow')
else:
- return _('%(number)s weeks')%{'number': int(days/7)}
- if self.day > 7:
- return _('1 week')
- if self.day > 1:
- return _('%(number)s days')%{'number': self.day}
- if self.day == 1 or self.hour > 12:
- return _('yesterday')
- if self.hour > 1:
- return _('%(number)s hours')%{'number': self.hour}
- if self.hour == 1:
+ return _('yesterday')
+ elif self.hour > 1:
+ s = _('%(number)s hours')%{'number': self.hour}
+ elif self.hour == 1:
if self.minute < 15:
- return _('an hour')
- quart = self.minute/15
- if quart == 2:
- return _('1 1/2 hours')
- return _('1 %(number)s/4 hours')%{'number': quart}
- if self.minute < 1:
- return _('just now')
- if self.minute == 1:
- return _('1 minute')
- if self.minute < 15:
- return _('%(number)s minutes')%{'number': self.minute}
- quart = int(self.minute/15)
- if quart == 2:
- return _('1/2 an hour')
- return _('%(number)s/4 hour')%{'number': quart}
+ s = _('an hour')
+ elif self.minute/15 == 2:
+ s = _('1 1/2 hours')
+ else:
+ s = _('1 %(number)s/4 hours')%{'number': self.minute/15}
+ elif self.minute < 1:
+ if self.sign > 0:
+ return _('in a moment')
+ else:
+ return _('just now')
+ elif self.minute == 1:
+ s = _('1 minute')
+ elif self.minute < 15:
+ s = _('%(number)s minutes')%{'number': self.minute}
+ elif int(self.minute/15) == 2:
+ 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):
- return (self.year, self.month, self.day, self.hour, self.minute,
- self.second)
+ return (self.sign, self.year, self.month, self.day, self.hour,
+ self.minute, self.second)
+ def serialise(self):
+ 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, allow_granularity=1, **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)
+
+ The Type parameter here should be class itself (e.g. Date), not a
+ class instance.
+
+ """
+ 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:
+ if allow_granularity:
+ self.from_value = Type(spec, **params)
+ self.to_value = Type(spec, add_granularity=1, **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", "from 18:00 TO +2m", "12:00;", "tO +3d",
+ "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
+ rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
+ for rspec in rspecs:
+ print '>>> Range("%s")' % rspec
+ print `Range(rspec, Date)`
+ print
+ for rspec in rispecs:
+ print '>>> Range("%s")' % rspec
+ print `Range(rspec, Interval)`
+ print
def test():
intervals = (" 3w 1 d 2:00", " + 2d", "3w")
print `Interval(interval)`
dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
- "08-13.22:13", "14:25")
+ "08-13.22:13", "14:25", '2002-12')
for date in dates:
print '>>> Date("%s")'%date
print `Date(date)`
if __name__ == '__main__':
test()
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.17 2002/01/16 07:02:57 richard
-# . lots of date/interval related changes:
-# - more relaxed date format for input
-#
-# Revision 1.16 2002/01/08 11:56:24 richard
-# missed an import _
-#
-# Revision 1.15 2002/01/05 02:27:00 richard
-# I18N'ification
-#
-# Revision 1.14 2001/11/22 15:46:42 jhermann
-# Added module docstrings to all modules.
-#
-# Revision 1.13 2001/09/18 22:58:37 richard
-#
-# Added some more help to roundu-admin
-#
-# Revision 1.12 2001/08/17 03:08:11 richard
-# fixed prettification of intervals of 1 week
-#
-# Revision 1.11 2001/08/15 23:43:18 richard
-# Fixed some isFooTypes that I missed.
-# Refactored some code in the CGI code.
-#
-# Revision 1.10 2001/08/07 00:24:42 richard
-# stupid typo
-#
-# Revision 1.9 2001/08/07 00:15:51 richard
-# Added the copyright/license notice to (nearly) all files at request of
-# Bizar Software.
-#
-# Revision 1.8 2001/08/05 07:46:12 richard
-# Changed date.Date to use regular string formatting instead of strftime -
-# win32 seems to have problems with %T and no hour... or something...
-#
-# Revision 1.7 2001/08/02 00:27:04 richard
-# Extended the range of intervals that are pretty-printed before actual dates
-# are displayed.
-#
-# Revision 1.6 2001/07/31 09:54:18 richard
-# Fixed the 2.1-specific gmtime() (no arg) call in roundup.date. (Paul Wright)
-#
-# Revision 1.5 2001/07/29 07:01:39 richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.4 2001/07/25 04:09:34 richard
-# Fixed offset handling (shoulda read the spec a little better)
-#
-# Revision 1.3 2001/07/23 07:56:05 richard
-# Storing only marshallable data in the db - no nasty pickled class references.
-#
-# Revision 1.2 2001/07/22 12:09:32 richard
-# Final commit of Grande Splite
-#
-# Revision 1.1 2001/07/22 11:58:35 richard
-# More Grande Splite
-#
-#
# vim: set filetype=python ts=4 sw=4 et si