Code

*** empty log message ***
[roundup.git] / roundup / date.py
index b9fd398c160b56d844192fb778bb890e928838b5..a8d6ef3b09817ea0963c0663e806a09eb0dfa5f5 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: date.py,v 1.57 2003-11-19 22:53:15 jlgijsbers 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 _
@@ -51,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 <Date 2000-04-17.00:00:00>
       "01-25" means <Date yyyy-01-25.00:00:00>
@@ -69,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(".")
         <Date 2000-06-26.00:34:02>
         >>> _.local(-5)
@@ -95,27 +95,37 @@ class Date:
     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
         '''
@@ -123,8 +133,10 @@ class Date:
         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
@@ -138,7 +150,11 @@ class Date:
             _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:
@@ -159,21 +175,26 @@ class Date:
             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
@@ -185,18 +206,19 @@ class Date:
         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:
@@ -204,13 +226,13 @@ class Date:
             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)
@@ -265,7 +287,8 @@ class Date:
             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
+        # intervals work in whole seconds
+        diff = int(a - b)
         if diff > 0:
             sign = 1
         else:
@@ -277,21 +300,29 @@ class Date:
         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...
@@ -307,7 +338,7 @@ class Date:
         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.
@@ -323,6 +354,14 @@ class Date:
         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
@@ -383,11 +422,13 @@ class Interval:
             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 -
@@ -655,20 +696,24 @@ def fixTimeOverflow(time):
     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>