diff --git a/roundup/date.py b/roundup/date.py
index 69c9935bb3fbd9286104461fec2d672ef25351ec..a8d6ef3b09817ea0963c0663e806a09eb0dfa5f5 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.55 2003-11-03 10:23:05 anthonybaxter Exp $
+# $Id: date.py,v 1.63 2004-03-24 04:57:25 richard Exp $
-__doc__ = """
-Date, time and time interval handling.
+"""Date, time and time interval handling.
"""
+__docformat__ = 'restructuredtext'
import time, re, calendar, types
from i18n import _
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 <Date 2000-04-17.00:00:00>
"01-25" means <Date yyyy-01-25.00:00:00>
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(".")
<Date 2000-06-26.00:34:02>
>>> _.local(-5)
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, add_granularity=add_granularity)
- else:
+ return
+ elif hasattr(spec, 'tuple'):
+ spec = spec.tuple()
+ try:
y,m,d,H,M,S,x,x,x = spec
+ frac = S - int(S)
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)
+ # we lost the fractional part
+ self.second = self.second + frac
+ except:
+ raise ValueError, 'Unknown spec %r'%spec
- usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS][offset]'
+ usagespec='[yyyy]-[mm]-[dd].[H]H:MM[:SS.SSS][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<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\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)
+ (\d{4})(\d\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])
+ g = m.groups()
+ (self.year, self.month, self.day, self.hour, self.minute) = \
+ map(int, g[:5])
+ self.second = float(g[5])
return
# not serialised data, try usual format
_add_granularity(info, 'SMHdmyab')
# get the current date as our default
- y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
+ ts = time.time()
+ frac = ts - int(ts)
+ y,m,d,H,M,S,x,x,x = time.gmtime(ts)
+ # gmtime loses the fractional seconds
+ S = S + frac
if info['y'] is not None or info['a'] is not None:
if info['y'] 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 info['S'] is not None:
+ S = float(info['S'])
if add_granularity:
S = S - 1
# now handle the adjustment of hour
+ frac = S - int(S)
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)
+ # we lost the fractional part along the way
+ self.second = self.second + frac
if info.get('o', None):
try:
self.applyInterval(Interval(info['o'], allowdate=0))
except ValueError:
- raise ValueError, _('Not a date spec: %s' % self.usagespec)
+ raise ValueError, _('%r not a date spec (%s)')%(spec,
+ self.usagespec)
def addInterval(self, interval):
''' Add the interval to this date, returning the date tuple
day = self.day + sign * interval.day
hour = self.hour + sign * interval.hour
minute = self.minute + sign * interval.minute
- second = self.second + sign * interval.second
+ # Intervals work on whole seconds
+ second = int(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):
+ 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 > 59: 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 > 12: year += 1; month -= 12
# now do the days, now that we know what month we're in
- def get_mdays(year,month):
+ 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 < 0 or day > get_mdays(year,month):
+
+ while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
# now to day under/over
- if day < 0:
+ if day < 1:
# When going backwards, decrement month, then increment days
month -= 1
day += get_mdays(year,month)
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
'''
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, ....
+ 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:
+ # intervals work in whole seconds
+ diff = int(a - b)
+ if diff > 0:
sign = 1
- diff = -diff
else:
sign = -1
+ diff = -diff
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)
+ H = (diff/(60*60))%24
+ d = diff/(24*60*60)
+ return Interval((0, 0, d, H, M, S), sign=sign)
- def __cmp__(self, other):
+ def __cmp__(self, other, int_seconds=0):
"""Compare this date to another date."""
if other is None:
return 1
- for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
+ for attr in ('year', 'month', 'day', 'hour', 'minute'):
if not hasattr(other, attr):
return 1
r = cmp(getattr(self, attr), getattr(other, attr))
if r: return r
- return 0
+ if not hasattr(other, 'second'):
+ return 1
+ if int_seconds:
+ return cmp(int(self.second), int(other.second))
+ return cmp(self.second, other.second)
def __str__(self):
"""Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
- return '%4d-%02d-%02d.%02d:%02d:%02d'%(self.year, self.month, self.day,
- self.hour, self.minute, self.second)
+ return self.formal()
+
+ def formal(self, sep='.', sec='%02d'):
+ f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
+ return f%(self.year, self.month, self.day, self.hour, self.minute,
+ self.second)
def pretty(self, format='%d %B %Y'):
''' print up the date date using a pretty format...
return str
def __repr__(self):
- return '<Date %s>'%self.__str__()
+ return '<Date %s>'%self.formal(sec='%f')
def local(self, offset):
""" Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
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 '''
+ frac = self.second - int(self.second)
+ ts = calendar.timegm((self.year, self.month, self.day, self.hour,
+ self.minute, self.second, 0, 0, 0))
+ # we lose the fractional part
+ return ts + frac
+
class Interval:
'''
Date intervals are specified using the suffixes "y", "m", and "d". The
if len(spec) == 7:
self.sign, self.year, self.month, self.day, self.hour, \
self.minute, self.second = spec
+ self.second = int(self.second)
else:
# old, buggy spec form
self.sign = sign
self.year, self.month, self.day, self.hour, self.minute, \
self.second = spec
+ self.second = int(self.second)
def set(self, spec, allowdate=1, interval_re=re.compile('''
\s*(?P<s>[-+])? # + or -
raise TypeError, "Can't add %r"%other
def __div__(self, other):
- ''' Divide this interval by an int value.
+ """ 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:
'''
if self.year:
if self.year == 1:
- return _('1 year')
+ s = _('1 year')
else:
- return _('%(number)s years')%{'number': self.year}
+ s = _('%(number)s years')%{'number': self.year}
elif self.month or self.day > 13:
days = (self.month * 30) + self.day
if days > 28:
self.day, self.hour, self.minute, self.second)
def fixTimeOverflow(time):
- ''' Handle the overflow in the time portion (H, M, S) of "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...
return (sign, y, m, d, H, M, S)
class Range:
- """
- Represents range between two values
+ """Represents range between two values
Ranges can be created using one of theese two alternative syntaxes:
- 1. Native english syntax:
+ 1. Native english syntax::
+
[[From] <value>][ To <value>]
- Keywords "From" and "To" are case insensitive. Keyword "From" is optional.
- 2. "Geek" syntax:
- [<value>][; <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):
+ 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>