X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fdate.py;h=8f60a725e7eddfb56e73bea99a234b59931f6eda;hb=53ea64cdb21d7beee1f69c3b7057d97ec4e84a74;hp=fac763db6dd9aba6f9d35809bb143860c8a3d0a1;hpb=f8321089b2a445b0ee36ade9b6e65924073eff6b;p=roundup.git diff --git a/roundup/date.py b/roundup/date.py index fac763d..8f60a72 100644 --- a/roundup/date.py +++ b/roundup/date.py @@ -15,9 +15,23 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: date.py,v 1.11 2001-08-15 23:43:18 richard Exp $ +# $Id: date.py,v 1.60 2004-02-11 23:55:08 richard Exp $ -import time, re, calendar +"""Date, time and time interval handling. +""" +__docformat__ = 'restructuredtext' + +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: ''' @@ -37,6 +51,7 @@ class Date: care of these conversions. In the following examples, suppose that yyyy is the current year, mm is the current month, and dd is the current day of the month; and suppose that the user is on Eastern Standard Time. + Examples:: "2000-04-17" means "01-25" means @@ -45,6 +60,8 @@ class Date: "11-07.09:32:43" means "14:25" means "8:47:11" means + "2003" means + "2003-06" means "." means "right now" The Date class should understand simple date expressions of the form @@ -53,9 +70,8 @@ class Date: separately. For example, when evaluating "2000-06-25 + 1m 10d", we first add one month to get 2000-07-25, then add 10 days to get 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40 - or 41 days). + or 41 days). Example usage:: - Example usage: >>> Date(".") >>> _.local(-5) @@ -70,85 +86,206 @@ class Date: >>> Date("14:25", -5) + + 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 - added or subtracted interval. Or a date 9-tuple. - 'offset' is the local time zone offset from GMT in hours. + 'spec' + is a full date or a partial form, with an optional added or + subtracted interval. Or a date 9-tuple. + '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\d\d\d\d)([/-](?P\d\d?)([/-](?P\d\d?))?)? # yyyy[-mm[-dd]] + |(?P\d\d?)[/-](?P\d\d?))? # or mm-dd + (?P\.)? # . + (((?P\d?\d):(?P\d\d))?(:(?P\d\d))?)? # hh:mm:ss + (?P.+)? # 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 > 23): + 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 > 23: 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 + def get_mdays(year, month): + if month == 2 and calendar.isleap(year): return 29 + else: return calendar.mdays[month] + + while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month): + # now to day under/over + if day < 1: + # When going backwards, decrement month, then increment days + month -= 1 + day += get_mdays(year,month) + elif day > get_mdays(year,month): + # When going forwards, decrement days, then increment month + day -= get_mdays(year,month) + month += 1 + + # possibly fix up the month so we're within range + while month < 1 or month > 12: + if month < 1: year -= 1; month += 12 ; day += 31 + if month > 12: year += 1; month -= 12 + + return (year, month, day, hour, minute, second, 0, 0, 0) + + def differenceDate(self, other): + "Return the difference between this date and another date" + 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' + + return self.dateDelta(other) + + def dateDelta(self, other): + """ Produce an Interval of the difference between this date + and another date. Only returns days:hours:minutes:seconds. + """ + # Returning intervals larger than a day is almost + # impossible - months, years, weeks, are all so imprecise. + 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 + else: + sign = -1 + diff = -diff + S = diff%60 + M = (diff/60)%60 + H = (diff/(60*60))%24 + d = diff/(24*60*60) + return Interval((0, 0, 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 @@ -158,64 +295,41 @@ class 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... - ''' - return time.strftime('%e %B %Y', (self.year, self.month, - self.day, self.hour, self.minute, self.second, 0, 0, 0)) - def set(self, spec, offset=0, date_re=re.compile(r''' - (((?P\d\d\d\d)-)?((?P\d\d)-(?P\d\d))?)? # yyyy-mm-dd - (?P\.)? # . - (((?P\d?\d):(?P\d\d))?(:(?P\d\d))?)? # hh:mm:ss - (?P.+)? # 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 ''%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) + + def timestamp(self): + ''' return a UNIX timestamp for this date ''' + return calendar.timegm((self.year, self.month, self.day, self.hour, + self.minute, self.second, 0, 0, 0)) + class Interval: ''' Date intervals are specified using the suffixes "y", "m", and "d". The @@ -233,30 +347,132 @@ class Interval: Example usage: >>> Interval(" 3w 1 d 2:00") - - >>> Date(". + 2d") - Interval("3w") + + >>> Date(". + 2d") + Interval("- 3w") + >>> Interval('1:59:59') + Interval('00:00:01') + + >>> Interval('2:00') + Interval('- 00:00:01') + + >>> Interval('1y')/2 + + >>> Interval('1:00')/2 + + >>> Interval('2003-03-18') + + >>> Interval('-4d 2003-03-18') + + + 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[-+])? # + or - + \s*((?P\d+\s*)y)? # year + \s*((?P\d+\s*)m)? # month + \s*((?P\d+\s*)w)? # week + \s*((?P\d+\s*)d)? # day + \s*(((?P\d+):(?P\d+))?(:(?P\d+))?)? # time + \s*(?P + (\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[+-])?1?(?P([ ]{3}\d|\d{4}))(?P\d{2})(?P\d{2}) + (?P\d{2})(?P\d{2})(?P\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.""" - for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'): + if other is None: + # we are always larger than None + return 1 + 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) @@ -264,95 +480,277 @@ class Interval: 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[-+])? # + or - - \s* - ((?P\d+\s*)y)? # year - \s* - ((?P\d+\s*)m)? # month - \s* - ((?P\d+\s*)w)? # week - \s* - ((?P\d+\s*)d)? # day - \s* - (((?P\d?\d):(?P\d\d))?(:(?P\d\d))?)? # time - \s* - ''', re.VERBOSE)): - ''' 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 = interval_re.match(spec) - if not m: - raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]' + 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 - 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])) + 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" - if self.week: - self.day = self.day + self.week*7 + 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 info['s'] is not None: - self.sign = {'+':1, '-':-1}[info['s']] + 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 __repr__(self): return ''%self.__str__() - def pretty(self, threshold=('d', 5)): + def pretty(self): ''' print up the date date using one of these nice formats.. - < 1 minute - < 15 minutes - < 30 minutes - < 1 hour - < 12 hours - < 1 day - otherwise, return None (so a full date may be displayed) ''' - if self.year or self.month > 2: - return None - if self.month: + if self.year: + if self.year == 1: + s = _('1 year') + else: + s = _('%(number)s years')%{'number': self.year} + elif self.month or self.day > 13: days = (self.month * 30) + self.day if days > 28: - return '%s months'%int(days/30) + if int(days/30) > 1: + s = _('%(number)s months')%{'number': int(days/30)} + else: + 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 '%s weeks'%int(days/7) - if self.day > 7: - return '%s weeks'%self.day - if self.day > 1: - return '%s days'%self.day - if self.day == 1 or self.hour > 12: - return 'yesterday' - if self.hour > 1: - return '%s hours'%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 %s/4 hours'%quart - if self.minute < 1: - return 'just now' - if self.minute == 1: - return '1 minute' - if self.minute < 15: - return '%s minutes'%self.minute - quart = int(self.minute/15) - if quart == 2: - return '1/2 an hour' - return '%s/4 hour'%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] ][ To ] + + Keywords "From" and "To" are case insensitive. Keyword "From" is + optional. + + 2. "Geek" syntax:: + + [][; ] + + Either first or second 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("18:00 TO +2m") + + + >>> Range("12:00") + + + >>> Range("tO +3d") + + + >>> Range("2002-11-10; 2002-12-12") + + + >>> Range("; 20:00 +1d") + + + """ + def __init__(self, spec, Type, allow_granularity=1, **params): + """Initializes Range of type from given 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 "" % 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") @@ -361,7 +759,7 @@ def test(): 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)` @@ -374,40 +772,4 @@ def test(): if __name__ == '__main__': test() -# -# $Log: not supported by cvs2svn $ -# 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