diff --git a/roundup/date.py b/roundup/date.py
index 4bdf11125a19b23fab63dc34adf88ea956408ade..c170fb6b3ac3c61c5f2921ce8fc984cf47b572aa 100644 (file)
--- a/roundup/date.py
+++ b/roundup/date.py
# under the same terms as Python, so long as this copyright message and
# disclaimer are retained in their original form.
#
-# IN NO EVENT SHALL THE BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# 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.
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: date.py,v 1.9 2001-08-07 00:15:51 richard Exp $
+# $Id: date.py,v 1.20 2002-02-21 23:34:51 richard Exp $
+
+__doc__ = """
+Date, time and time interval handling.
+"""
import time, re, calendar
+from i18n import _
class Date:
'''
>>> Date("14:25", -5)
<Date 2000-06-25.19:25:00>
'''
- isDate = 1
-
def __init__(self, spec='.', offset=0):
"""Construct a date given a specification and a time zone offset.
self.second, x, x, x = time.gmtime(calendar.timegm(t))
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)))
+ """Add an interval to this date to produce another date.
+ """
+ # do the basic calc
+ sign = other.sign
+ year = self.year + sign * other.year
+ month = self.month + sign * other.month
+ day = self.day + sign * other.day
+ hour = self.hour + sign * other.hour
+ minute = self.minute + sign * other.minute
+ second = self.second + sign * other.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 Date((year, month, day, hour, minute, second, 0, 0, 0))
# XXX deviates from spec to allow subtraction of dates as well
def __sub__(self, other):
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(), sign=-other.sign)
+ 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'):
r = cmp(getattr(self, attr), getattr(other, attr))
if r: return r
def pretty(self):
''' print up the date date using a pretty format...
'''
- return time.strftime('%e %B %Y', (self.year, self.month,
+ 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<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
'''
m = date_re.match(spec)
if not m:
- raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]'
+ 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
Example usage:
>>> Interval(" 3w 1 d 2:00")
<Interval 22d 2:00>
- >>> Date(". + 2d") - Interval("3w")
+ >>> Date(". + 2d") + Interval("- 3w")
<Date 2000-06-07.00:34:02>
- '''
- 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.
+
+ 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(''):
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'):
r = cmp(getattr(self, attr), getattr(other, attr))
if r: return r
\s*
((?P<d>\d+\s*)d)? # day
\s*
- (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)? # time
+ (((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
\s*
''', re.VERBOSE)):
''' set the date to the value in spec
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]'
+ 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',
def __repr__(self):
return '<Interval %s>'%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:
+ 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:
- 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:
+ 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 = 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)}
+ return s
def get_tuple(self):
return (self.year, self.month, self.day, self.hour, self.minute,
#
# $Log: not supported by cvs2svn $
+# Revision 1.19 2002/02/21 23:11:45 richard
+# . fixed some problems in date calculations (calendar.py doesn't handle over-
+# and under-flow). Also, hour/minute/second intervals may now be more than
+# 99 each.
+#
+# Revision 1.18 2002/01/23 20:00:50 jhermann
+# %e is a UNIXism and not documented for Python
+#
+# 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...