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.
 # 
 # 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 _
 
 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.
     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>
 
       "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
     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)
         >>> 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.
 
     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)
         """
         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
             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)
             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>\.)?                                     # .
     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'''
             (?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
         '''
             ''', 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!
         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
             return
 
         # not serialised data, try usual format
@@ -138,7 +150,11 @@ class Date:
             _add_granularity(info, 'SMHdmyab')
 
         # get the current date as our default
             _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:
 
         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
             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
 
         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)
         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:
 
         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
 
     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
         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
 
         # 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
             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:
 
         # 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
             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]
             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
             # 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)
                 # 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))
             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:
         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)
 
         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
         """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
             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."""
 
     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...
 
     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 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.
 
     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)
 
         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
 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
             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
             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 -
 
     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:
     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:
         
     Ranges can be created using one of theese two alternative syntaxes:
         
-        1. Native english syntax: 
+    1. Native english syntax::
+
             [[From] <value>][ To <value>]
             [[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.
 
 
     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>
         
         >>> Range("from 2-12 to 4-2")
         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>