[Python-checkins] python/nondist/sandbox/datetime datetime.py,1.64,1.65 test_datetime.py,1.45,1.46

tim_one@users.sourceforge.net tim_one@users.sourceforge.net
Mon, 25 Nov 2002 13:09:11 -0800


Update of /cvsroot/python/python/nondist/sandbox/datetime
In directory sc8-pr-cvs1:/tmp/cvs-serv2016

Modified Files:
	datetime.py test_datetime.py 
Log Message:
SF patch 641958:  time and timetz for the datetime module,
from Marius Gedminas.  This is subject to change, as the design is still
being discussed on the wiki:
http://www.zope.org/Members/fdrake/DateTimeWiki/TimeType


Index: datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/datetime.py,v
retrieving revision 1.64
retrieving revision 1.65
diff -C2 -d -r1.64 -r1.65
*** datetime.py	22 Nov 2002 03:34:42 -0000	1.64
--- datetime.py	25 Nov 2002 21:09:05 -0000	1.65
***************
*** 694,697 ****
--- 694,1000 ----
  
  
+ class time(object):
+     """Concrete time type.
+ 
+     Constructors:
+ 
+     __init__()
+ 
+     Operators:
+ 
+     __repr__, __str__
+     __cmp__, __hash__
+ 
+     Methods:
+ 
+     strftime()
+     isoformat()
+ 
+     Properties (readonly):
+     hour, minute, second, microsecond
+     """
+ 
+     def __init__(self, hour, minute, second=0, microsecond=0):
+         """Constructor.
+ 
+         Arguments:
+ 
+         hour, minute (required)
+         second, microsecond (default to zero)
+         """
+         if not 0 <= hour <= 23:
+             raise ValueError('hour must be in 0..23', hour)
+         if not 0 <= minute <= 59:
+             raise ValueError('minute must be in 0..59', minute)
+         if not 0 <= second <= 59:
+             raise ValueError('second must be in 0..59', second)
+         if not 0 <= microsecond <= 999999:
+             raise ValueError('microsecond must be in 0..999999', microsecond)
+         self.__hour = hour
+         self.__minute = minute
+         self.__second = second
+         self.__microsecond = microsecond
+ 
+     # Read-only field accessors
+     hour = property(lambda self: self.__hour, doc="hour (0-23)")
+     minute = property(lambda self: self.__minute, doc="minute (0-59)")
+     second = property(lambda self: self.__second, doc="second (0-59)")
+     microsecond = property(lambda self: self.__microsecond,
+                            doc="microsecond (0-999999)")
+ 
+     # Standard conversions, __cmp__, __hash__ (and helpers)
+ 
+     def __cmp__(self, other):
+         """Three-way comparison."""
+         if isinstance(other, time):
+             return cmp((self.__hour, self.__minute, self.__second,
+                         self.__microsecond),
+                        (other.__hour, other.__minute, other.__second,
+                         other.__microsecond))
+         raise TypeError, ("can't compare time to %s instance" %
+                           type(other).__name__)
+ 
+     def __hash__(self):
+         """Hash."""
+         return hash((self.__hour, self.__minute, self.__second,
+                      self.__microsecond))
+ 
+     # Conversions to string
+ 
+     def __repr__(self):
+         """Convert to formal string, for repr()."""
+         if self.__microsecond != 0:
+             s = ", %d, %d" % (self.__second, self.__microsecond)
+         elif self.__second != 0:
+             s = ", %d" % self.__second
+         else:
+             s = ""
+         return "%s(%d, %d%s)" % (self.__class__.__name__,
+                                  self.__hour, self.__minute, s)
+ 
+     def __str__(self):
+         """Convert to pretty string, for str()."""
+         pretty = "%d:%02d:%02d.%06d" % (
+             self.__hour, self.__minute, self.__second,
+             self.__microsecond)
+         # trim microseconds: hh:mm:ss.xxx000 -> hh:mm:ss.xxx
+         while pretty.endswith('0'):
+             pretty = pretty[:-1]
+         # trim microseconds: hh:mm:ss.000000 -> hh:mm:ss
+         if pretty.endswith('.'):
+             pretty = pretty[:-1]
+         # trim seconds: hh:mm:00 -> hh:mm
+         if pretty.endswith(':00'):
+             pretty = pretty[:-3]
+         return pretty
+ 
+     def isoformat(self):
+         """Return the time formatted according to ISO.
+ 
+         This is 'HH:MM:SS.mmmmmm'.
+         """
+         return "%02d:%02d:%02d.%06d" % (
+             self.__hour, self.__minute, self.__second,
+             self.__microsecond)
+ 
+     def strftime(self, fmt):
+         """Format using strftime().  The date part of the timestamp passed
+         to underlying strftime should not be used.
+         """
+         return _time.strftime(fmt, (0, 0, 0, self.__hour, self.__minute,
+                                     self.__second, 0, 0, -1))
+ 
+ 
+ time.min = time(0, 0, 0)
+ time.max = time(23, 59, 59, 999999)
+ time.resolution = timedelta(microseconds=1)
+ 
+ 
+ class timetz(time):
+     """Time with time zone.
+ 
+     Constructors:
+ 
+     __init__()
+ 
+     Operators:
+ 
+     __repr__, __str__
+     __cmp__, __hash__
+ 
+     Methods:
+ 
+     strftime()
+     isoformat()
+     utcoffset()
+     tzname()
+     dst()
+ 
+     Properties (readonly):
+     hour, minute, second, microsecond, tzinfo
+     """
+ 
+     def __init__(self, hour, minute, second=0, microsecond=0, tzinfo=None):
+         """Constructor.
+ 
+         Arguments:
+ 
+         hour, minute (required)
+         second, microsecond (default to zero)
+         tzinfo (default to None)
+         """
+         super(timetz, self).__init__(hour, minute, second, microsecond)
+         if tzinfo is not None:
+             # Better fail now than later
+             assert hasattr(tzinfo, 'utcoffset')
+             assert hasattr(tzinfo, 'dst')
+             assert hasattr(tzinfo, 'tzname')
+         self.__tzinfo = tzinfo
+ 
+     # Read-only field accessors
+     tzinfo = property(lambda self: self.__tzinfo, doc="timezone info object")
+ 
+     # Standard conversions, __cmp__, __hash__ (and helpers)
+ 
+     def __cmp__(self, other):
+         """Three-way comparison."""
+         if not isinstance(other, time):
+             raise TypeError("can't compare timetz to %s instance" %
+                             type(other).__name__)
+         superself = super(timetz, self)
+         supercmp = superself.__cmp__
+         mytz = self.__tzinfo
+         ottz = None
+         if isinstance(other, timetz):
+             ottz = other.__tzinfo
+         if mytz is ottz:
+             return supercmp(other)
+         myoff = otoff = None
+         if mytz is not None:
+             myoff = mytz.utcoffset(self)
+         if ottz is not None:
+             otoff = ottz.utcoffset(other)
+         if myoff == otoff:
+             return supercmp(other)
+         if myoff is None or otoff is None:
+             raise ValueError, "cannot mix naive and timezone-aware time"
+         myhhmm = self.hour * 60 + self.minute - myoff
+         othhmm = other.hour * 60 + other.minute - otoff
+         return cmp((myhhmm, self.second, self.microsecond),
+                    (othhmm, other.second, other.microsecond))
+ 
+     def __hash__(self):
+         """Hash."""
+         tz = self.__tzinfo
+         if tz == None:
+             return super(timetz, self).__hash__()
+         tzoff = tz.utcoffset(self)
+         if not tzoff: # zero or None!
+             return super(timetz, self).__hash__()
+         h, m = divmod(self.hour * 60 + self.minute - tzoff, 60)
+         # Unfortunately it is not possible to construct a new timetz object
+         # and use super().__hash__(), since hour may exceed the range of
+         # allowed values
+         return hash((h, m, self.second, self.microsecond))
+ 
+     # Conversion to string
+ 
+     def _tzstr(self, sep=":"):
+         """Return formatted timezone offset (+xx:xx) or None."""
+         if self.__tzinfo is not None:
+             off = self.__tzinfo.utcoffset(self)
+             if off is not None:
+                 if off < 0:
+                     sign = "-"
+                     off = -off
+                 else:
+                     sign = "+"
+                 hh, mm = divmod(off, 60)
+                 return "%s%02d%s%02d" % (sign, hh, sep, mm)
+ 
+     def __repr__(self):
+         """Convert to formal string, for repr()."""
+         s = super(timetz, self).__repr__()
+         if self.__tzinfo is not None:
+             assert s[-1:] == ")"
+             s = s[:-1] + ", tzinfo=%r" % self.__tzinfo + ")"
+         return s
+ 
+     def __str__(self):
+         """Convert to pretty string, for str()."""
+         s = super(timetz, self).__str__()
+         tz = self._tzstr()
+         if tz: s = "%s %s" % (s, tz)
+         return s
+ 
+     def isoformat(self):
+         """Return the time formatted according to ISO.
+ 
+         This is 'HH:MM:SS.mmmmmm+zz:zz'.
+         """
+         s = super(timetz, self).isoformat()
+         tz = self._tzstr()
+         if tz: s += tz
+         return s
+ 
+     def strftime(self, fmt):
+         """Format using strftime().  The date part of the timestamp passed
+         to underlying strftime should not be used.
+ 
+         You can use %Z to refer to the timezone name and %z to refer to its
+         UTC offset (+zzzz).
+         """
+         tz = self._tzstr(sep="")
+         if tz:
+             fmt = fmt.replace("%z", tz).replace("%Z", self.tzinfo.tzname(None))
+         else:
+             fmt = fmt.replace("%z", "").replace("%Z", "")
+         return super(timetz, self).strftime(fmt)
+ 
+     # Timezone functions
+ 
+     def utcoffset(self):
+         """Return the timezone offset in minutes east of UTC (negative west of
+         UTC)."""
+         tz = self.__tzinfo
+         if tz is None:
+             return None
+         else:
+             return tz.utcoffset(self)
+ 
+     def tzname(self):
+         """Return the timezone name.
+ 
+         Note that the name is 100% informational -- there's no requirement that
+         it mean anything in particular. For example, "GMT", "UTC", "-500",
+         "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies.
+         """
+         tz = self.__tzinfo
+         if tz is None:
+             return None
+         else:
+             return tz.tzname(self)
+ 
+     def dst(self):
+         """Return 0 if DST is not in effect, or the DST offset (in minutes
+         eastward) if DST is in effect.
+ 
+         This is purely informational; the DST offset has already been added to
+         the UTC offset returned by utcoffset() if applicable, so there's no
+         need to consult dst() unless you're interested in displaying the DST
+         info.
+         """
+         tz = self.__tzinfo
+         if tz is None:
+             return None
+         else:
+             return tz.dst(self)
+ 
+ 
+ timetz.min = timetz(0, 0, 0)
+ timetz.max = timetz(23, 59, 59, 999999)
+ timetz.resolution = timedelta(microseconds=1)
+ 
+ 
  class datetime(date):
      """Concrete date/time type, inheriting from date.
***************
*** 778,781 ****
--- 1081,1090 ----
      utcnow = classmethod(utcnow)
  
+     def combine(cls, date, time):
+         "Construct a datetime from a given date and a given time."
+         return cls(date.year, date.month, date.day,
+                    time.hour, time.minute, time.second, time.microsecond)
+     combine = classmethod(combine)
+ 
      # Conversions to string
  
***************
*** 808,811 ****
--- 1117,1129 ----
                  self.weekday(), self._yday(), -1)
  
+     def date(self):
+         "Return the date part."
+         return date(self.__year, self.__month, self.__day)
+ 
+     def time(self):
+         "Return the time part."
+         return time(self.__hour, self.__minute, self.__second,
+                     self.__microsecond)
+ 
      def __cmp__(self, other):
          "Three-way comparison."
***************
*** 942,945 ****
--- 1260,1271 ----
      now = classmethod(now)
  
+     def combine(cls, date, time):
+         "Construct a datetime from a given date and a given time."
+         return cls(date.year, date.month, date.day,
+                    time.hour, time.minute, time.second, time.microsecond,
+                    getattr(time, 'tzinfo', None))
+     combine = classmethod(combine)
+ 
+ 
      def utctimetuple(self):
          "Return UTC time tuple compatible with time.gmtime()."
***************
*** 952,955 ****
--- 1278,1286 ----
          dt = timedelta(minutes=offset)
          return (ts - dt).timetuple()
+ 
+     def timetz(self):
+         "Return the time part."
+         return timetz(self.hour, self.minute, self.second, self.microsecond,
+                       self.__tzinfo)
  
      def isoformat(self, sep=' '):

Index: test_datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/test_datetime.py,v
retrieving revision 1.45
retrieving revision 1.46
diff -C2 -d -r1.45 -r1.46
*** test_datetime.py	20 Nov 2002 03:29:43 -0000	1.45
--- test_datetime.py	25 Nov 2002 21:09:06 -0000	1.46
***************
*** 7,11 ****
  import unittest
  
! from datetime import date, datetime, datetimetz, timedelta, MINYEAR, MAXYEAR
  
  
--- 7,12 ----
  import unittest
  
! from datetime import date, time, timetz, datetime, datetimetz, timedelta, \
!                      MINYEAR, MAXYEAR
  
  
***************
*** 327,330 ****
--- 328,490 ----
              self.assertEqual(t, (1956, 3, 1+i, 0, 0, 0, (3+i)%7, 61+i, -1))
  
+ 
+ class TestTime(unittest.TestCase):
+ 
+     theclass = time
+ 
+     def test_basic_attributes(self):
+         t = self.theclass(12, 0)
+         self.assertEqual(t.hour, 12)
+         self.assertEqual(t.minute, 0)
+         self.assertEqual(t.second, 0)
+         self.assertEqual(t.microsecond, 0)
+ 
+     def test_basic_attributes_nonzero(self):
+         # Make sure all attributes are non-zero so bugs in
+         # bit-shifting access show up.
+         t = self.theclass(12, 59, 59, 8000)
+         self.assertEqual(t.hour, 12)
+         self.assertEqual(t.minute, 59)
+         self.assertEqual(t.second, 59)
+         self.assertEqual(t.microsecond, 8000)
+ 
+     def test_roundtrip(self):
+         for t in (self.theclass(1, 2, 3, 4),):
+             # Verify t -> string -> time identity.
+             s = repr(t)
+             t2 = eval(s)
+             self.assertEqual(t, t2)
+ 
+             # Verify identity via reconstructing from pieces.
+             t2 = self.theclass(t.hour, t.minute, t.second,
+                                t.microsecond)
+             self.assertEqual(t, t2)
+ 
+     def test_comparing(self):
+         t1 = self.theclass(9, 0, 0)
+         t2 = self.theclass(10, 0, 0)
+         t3 = self.theclass(9, 0, 0)
+         self.assertEqual(t1, t3)
+         self.assert_(t2 > t3)
+ 
+     def test_bad_constructor_arguments(self):
+         # bad hours
+         self.theclass(0, 0)    # no exception
+         self.theclass(23, 0)   # no exception
+         self.assertRaises(ValueError, self.theclass, -1, 0)
+         self.assertRaises(ValueError, self.theclass, 24, 0)
+         # bad minutes
+         self.theclass(23, 0)    # no exception
+         self.theclass(23, 59)   # no exception
+         self.assertRaises(ValueError, self.theclass, 23, -1)
+         self.assertRaises(ValueError, self.theclass, 23, 60)
+         # bad seconds
+         self.theclass(23, 59, 0)    # no exception
+         self.theclass(23, 59, 59)   # no exception
+         self.assertRaises(ValueError, self.theclass, 23, 59, -1)
+         self.assertRaises(ValueError, self.theclass, 23, 59, 60)
+         # bad microseconds
+         self.theclass(23, 59, 59, 0)        # no exception
+         self.theclass(23, 59, 59, 999999)   # no exception
+         self.assertRaises(ValueError, self.theclass, 23, 59, 59, -1)
+         self.assertRaises(ValueError, self.theclass, 23, 59, 59, 1000000)
+ 
+     def test_hash_equality(self):
+         d = self.theclass(23, 30, 17)
+         e = self.theclass(23, 30, 17)
+         self.assertEqual(d, e)
+         self.assertEqual(hash(d), hash(e))
+ 
+         dic = {d: 1}
+         dic[e] = 2
+         self.assertEqual(len(dic), 1)
+         self.assertEqual(dic[d], 2)
+         self.assertEqual(dic[e], 2)
+ 
+         d = self.theclass(0,  5, 17)
+         e = self.theclass(0,  5, 17)
+         self.assertEqual(d, e)
+         self.assertEqual(hash(d), hash(e))
+ 
+         dic = {d: 1}
+         dic[e] = 2
+         self.assertEqual(len(dic), 1)
+         self.assertEqual(dic[d], 2)
+         self.assertEqual(dic[e], 2)
+ 
+     def test_isoformat(self):
+         t = self.theclass(4, 5, 1, 123)
+         self.assertEqual(t.isoformat(), "04:05:01.000123")
+ 
+     def test_strftime(self):
+         t = self.theclass(1, 2, 3, 4)
+         self.assertEqual(t.strftime('%H %M %S'), "01 02 03")
+ 
+     def test_str(self):
+         self.assertEqual(str(self.theclass(1, 2, 3, 4)), "1:02:03.000004")
+         self.assertEqual(str(self.theclass(10, 2, 3, 4000)), "10:02:03.004")
+         self.assertEqual(str(self.theclass(0, 2, 3, 400000)), "0:02:03.4")
+         self.assertEqual(str(self.theclass(12, 2, 3, 0)), "12:02:03")
+         self.assertEqual(str(self.theclass(23, 15, 0, 0)), "23:15")
+ 
+     def test_repr(self):
+         self.assertEqual(repr(self.theclass(1, 2, 3, 4)),
+                          "%s(1, 2, 3, 4)" % self.theclass.__name__)
+         self.assertEqual(repr(self.theclass(10, 2, 3, 4000)),
+                          "%s(10, 2, 3, 4000)" % self.theclass.__name__)
+         self.assertEqual(repr(self.theclass(0, 2, 3, 400000)),
+                          "%s(0, 2, 3, 400000)" % self.theclass.__name__)
+         self.assertEqual(repr(self.theclass(12, 2, 3, 0)),
+                          "%s(12, 2, 3)" % self.theclass.__name__)
+         self.assertEqual(repr(self.theclass(23, 15, 0, 0)),
+                          "%s(23, 15)" % self.theclass.__name__)
+ 
+     def test_resolution_info(self):
+         self.assert_(isinstance(self.theclass.min, self.theclass))
+         self.assert_(isinstance(self.theclass.max, self.theclass))
+         self.assert_(isinstance(self.theclass.resolution, timedelta))
+         self.assert_(self.theclass.max > self.theclass.min)
+ 
+ 
+ class TestTimeTZ(TestTime):
+ 
+     theclass = timetz
+ 
+     def test_zones(self):
+         est = FixedOffset(-300, "EST")
+         utc = FixedOffset(0, "UTC")
+         met = FixedOffset(60, "MET")
+         t1 = timetz( 7, 47, tzinfo=est)
+         t2 = timetz(12, 47, tzinfo=utc)
+         t3 = timetz(13, 47, tzinfo=met)
+         self.assertEqual(t1.tzinfo, est)
+         self.assertEqual(t2.tzinfo, utc)
+         self.assertEqual(t3.tzinfo, met)
+         self.assertEqual(t1.utcoffset(), -300)
+         self.assertEqual(t2.utcoffset(), 0)
+         self.assertEqual(t3.utcoffset(), 60)
+         self.assertEqual(t1.tzname(), "EST")
+         self.assertEqual(t2.tzname(), "UTC")
+         self.assertEqual(t3.tzname(), "MET")
+         self.assertEqual(hash(t1), hash(t2))
+         self.assertEqual(hash(t1), hash(t3))
+         self.assertEqual(hash(t2), hash(t3))
+         self.assertEqual(t1, t2)
+         self.assertEqual(t1, t3)
+         self.assertEqual(t2, t3)
+         self.assertEqual(str(t1), "7:47 -05:00")
+         self.assertEqual(str(t2), "12:47 +00:00")
+         self.assertEqual(str(t3), "13:47 +01:00")
+         self.assertEqual(repr(t1), "timetz(7, 47, tzinfo=est)")
+         self.assertEqual(repr(t2), "timetz(12, 47, tzinfo=utc)")
+         self.assertEqual(repr(t3), "timetz(13, 47, tzinfo=met)")
+         self.assertEqual(t1.isoformat(), "07:47:00.000000-05:00")
+         self.assertEqual(t2.isoformat(), "12:47:00.000000+00:00")
+         self.assertEqual(t3.isoformat(), "13:47:00.000000+01:00")
+         self.assertEqual(t1.strftime("%H:%M:%S %Z %z"), "07:47:00 EST -0500")
+         self.assertEqual(t2.strftime("%H:%M:%S %Z %z"), "12:47:00 UTC +0000")
+         self.assertEqual(t3.strftime("%H:%M:%S %Z %z"), "13:47:00 MET +0100")
+ 
+ 
  class TestDateTime(TestDate):
  
***************
*** 531,534 ****
--- 691,705 ----
          self.assertEqual(t.ctime(), "Sat Mar  2 18:03:05 2002")
  
+     def test_combine(self):
+         d = date(2002, 3, 4)
+         t = time(18, 45, 3, 1234)
+         dt = datetime.combine(d, t)
+         self.assertEqual(dt, datetime(2002, 3, 4, 18, 45, 3, 1234))
+ 
+     def test_extract(self):
+         dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
+         self.assertEqual(dt.date(), date(2002, 3, 4))
+         self.assertEqual(dt.time(), time(18, 45, 3, 1234))
+ 
  
  class FixedOffset(object):
***************
*** 582,592 ****
                           "datetimetz(2002, 3, 19, 13, 47, tzinfo=met)")
  
  
  def test_suite():
      s1 = unittest.makeSuite(TestTimeDelta, 'test')
      s2 = unittest.makeSuite(TestDate, 'test')
!     s3 = unittest.makeSuite(TestDateTime, 'test')
!     s4 = unittest.makeSuite(TestDateTimeTZ, 'test')
!     return unittest.TestSuite([s1, s2, s3, s4])
  
  def test_main():
--- 753,780 ----
                           "datetimetz(2002, 3, 19, 13, 47, tzinfo=met)")
  
+     def test_combine(self):
+         met = FixedOffset(60, "MET")
+         d = date(2002, 3, 4)
+         tz = timetz(18, 45, 3, 1234, tzinfo=met)
+         dt = datetimetz.combine(d, tz)
+         self.assertEqual(dt, datetimetz(2002, 3, 4, 18, 45, 3, 1234,
+                                         tzinfo=met))
+ 
+     def test_extract(self):
+         met = FixedOffset(60, "MET")
+         dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234, tzinfo=met)
+         self.assertEqual(dt.date(), date(2002, 3, 4))
+         self.assertEqual(dt.time(), time(18, 45, 3, 1234))
+         self.assertEqual(dt.timetz(), timetz(18, 45, 3, 1234, tzinfo=met))
+ 
  
  def test_suite():
      s1 = unittest.makeSuite(TestTimeDelta, 'test')
      s2 = unittest.makeSuite(TestDate, 'test')
!     s3 = unittest.makeSuite(TestTime, 'test')
!     s4 = unittest.makeSuite(TestTimeTZ, 'test')
!     s5 = unittest.makeSuite(TestDateTime, 'test')
!     s6 = unittest.makeSuite(TestDateTimeTZ, 'test')
!     return unittest.TestSuite([s1, s2, s3, s4, s5, s6])
  
  def test_main():