X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fdate.py;h=d98e586989486348f7dd10d75b00e672c0057c75;hb=92940179f663af380c011a1df0bacbae2de56719;hp=d6feb4f260e66b3c92805af2918275064fa6d09c;hpb=ca118f58edb0cb8f18b6775de817d644e312efe6;p=roundup.git diff --git a/roundup/date.py b/roundup/date.py index d6feb4f..d98e586 100644 --- a/roundup/date.py +++ b/roundup/date.py @@ -1,6 +1,28 @@ -# $Id: date.py,v 1.4 2001-07-25 04:09:34 richard Exp $ +# +# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) +# This module is free software, and you may redistribute it and/or modify +# under the same terms as Python, so long as this copyright message and +# disclaimer are retained in their original form. +# +# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR +# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING +# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" +# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +# +# $Id: date.py,v 1.38 2002-12-16 04:39:36 richard Exp $ + +__doc__ = """ +Date, time and time interval handling. +""" import time, re, calendar +from i18n import _ class Date: ''' @@ -53,9 +75,11 @@ class Date: >>> Date("14:25", -5) - ''' - isDate = 1 + 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): """Construct a date given a specification and a time zone offset. @@ -71,106 +95,167 @@ class Date: self.year, self.month, self.day, self.hour, self.minute, \ self.second, x, x, x = time.gmtime(ts) + 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 other.isDate: - # 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 def __str__(self): """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format.""" - return time.strftime('%Y-%m-%d.%T', (self.year, self.month, self.day, - self.hour, self.minute, self.second, 0, 0, 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... + + Note that if the day is zero, and the day appears first in the + format, then the day number will be removed from output. ''' - return time.strftime('%e %B %Y', (self.year, self.month, - self.day, self.hour, self.minute, self.second, 0, 0, 0)) + 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\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)): + (((?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), 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 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 not m: - raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]' + 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 - y,m,d,H,M,S,x,x,x = time.gmtime() + # get the current date as our default + y,m,d = time.gmtime(time.time())[:3] + + # time defaults to 00:00:00 _always_ + H = M = S = 0 # 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']) # override hour, minute, second parts if info['H'] is not None and info['M'] is not None: @@ -184,23 +269,26 @@ class Date: self.year, self.month, self.day, self.hour, self.minute, \ self.second, x, x, x = time.gmtime(ts) - if info['o']: + if info.get('o', None): self.applyInterval(Interval(info['o'])) 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) + class Interval: ''' Date intervals are specified using the suffixes "y", "m", and "d". The @@ -219,27 +307,47 @@ class Interval: Example usage: >>> Interval(" 3w 1 d 2:00") - >>> Date(". + 2d") - Interval("3w") + >>> Date(". + 2d") + Interval("- 3w") - ''' - isInterval = 1 + Intervals are added/subtracted in order of: + seconds, minutes, hours, years, months, days + + Calculations involving monts (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): """Construct an interval given a specification.""" if type(spec) == type(''): self.set(spec) 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 __cmp__(self, other): """Compare this interval to another interval.""" + 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 - + def __str__(self): """Return this interval as a string.""" sign = {1:'+', -1:'-'}[self.sign] @@ -253,34 +361,48 @@ class Interval: l.append('%d:%02d'%(self.hour, self.minute)) 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)): + 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() + b = other.get_tuple() + if b[0] < 0: + i = Interval([x-y for x,y in zip(a[1:],b[1:])]) + else: + i = Interval([x+y for x,y in zip(a[1:],b[1:])]) + return i + # nope, no idea what to do with this other... + raise TypeError, "Can't add %r"%other + + 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+):(?P\d+))?(:(?P\d+))?)? # time + \s*''', 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)): ''' 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) + m = serialised_re.match(spec) if not m: - raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]' + m = interval_re.match(spec) + if not m: + raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] ' + '[#d] [[[H]H:MM]:SS]') 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: + if info.get(group, None) is not None: setattr(self, attr, int(info[group])) if self.week: @@ -292,45 +414,64 @@ class Interval: 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 or self.day > 5: - return None - 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: + 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: + 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 _('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 = 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)} + 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 test(): @@ -353,14 +494,4 @@ def test(): if __name__ == '__main__': test() -# -# $Log: not supported by cvs2svn $ -# 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