Make comparison and subtraction of aware objects ignore tzinfo if the
authorTim Peters <tim.peters@gmail.com>
Fri, 27 Dec 2002 00:41:11 +0000 (00:41 +0000)
committerTim Peters <tim.peters@gmail.com>
Fri, 27 Dec 2002 00:41:11 +0000 (00:41 +0000)
operands have identical tzinfo members (meaning object identity -- "is").
I misunderstood the intent here, reading wrong conclusion into
conflicting clues.

Doc/lib/libdatetime.tex
Lib/test/test_datetime.py
Modules/datetimemodule.c

index 1adbc8e863cc60abace76b5192c4432b5c270e07..32c0f93abbc6e7a336e21c2e28db3b567cabc8fe 100644 (file)
@@ -863,10 +863,15 @@ Supported operations:
 
 \begin{itemize}
   \item
-    comparison of \class{timetz} to timetz, where timetz1 is considered
-    less than timetz2 when timetz1 precedes timetz2 in time, and
-    where the \class{timetz} objects are first adjusted by subtracting
-    their UTC offsets (obtained from \method{utcoffset()}).
+    comparison of \class{timetz} to \class{time} or \class{timetz},
+    where \var{a} is considered less than \var{b} when \var{a} precedes
+    \var{b} in time.  If one comparand is naive and the other is aware,
+    \exception{TypeError} is raised.  If both comparands are aware, and
+    have the same \member{tzinfo} member, the common \member{tzinfo}
+    member is ignored and the base times are compared.  If both
+    comparands are aware and have different \member{tzinfo} members,
+    the comparands are first adjusted by subtracting their UTC offsets
+    (obtained from \code{self.utcoffset()}).
 
   \item
     hash, use as dict key
@@ -1011,11 +1016,13 @@ Supported operations:
   \item
     datetimetz1 + timedelta -> datetimetz2
     timedelta + datetimetz1 -> datetimetz2
+
     The same as addition of \class{datetime} objects, except that
     datetimetz2.tzinfo is set to datetimetz1.tzinfo.
 
   \item
     datetimetz1 - timedelta -> datetimetz2
+
     The same as addition of \class{datetime} objects, except that
     datetimetz2.tzinfo is set to datetimetz1.tzinfo.
 
@@ -1025,32 +1032,31 @@ Supported operations:
     \naive\_datetimetz1 - datetime2 -> timedelta
     datetime1 - \naive\_datetimetz2 -> timedelta
 
-  \item
-    Subtraction of a \class{datetime} or datetimetz, from a
+    Subtraction of a \class{datetime} or \class{datetimetz}, from a
     \class{datetime} or \class{datetimetz}, is defined only if both
-    operands are \naive, or if both are aware.  If one is aware and
-    the other is \naive, \exception{TypeError} is raised.
+    operands are \naive, or if both are aware.  If one is aware and the
+    other is \naive, \exception{TypeError} is raised.
 
-  \item
-    If both are \naive, subtraction acts as for \class{datetime}
-    subtraction.
+    If both are \naive, or both are aware and have the same \member{tzinfo}
+    member, subtraction acts as for \class{datetime} subtraction.
 
-  \item
-    If both are aware \class{datetimetz} objects, a-b acts as if a and b were
-    first converted to UTC datetimes (by subtracting \code{a.utcoffset()}
-    minutes from a, and \code{b.utcoffset()} minutes from b), and then doing
+    If both are aware and have different \member{tzinfo} members,
+    \code{a-b} acts as if \var{a} and \var{b} were first converted to UTC
+    datetimes (by subtracting \code{a.utcoffset()} minutes from \var{a},
+    and \code{b.utcoffset()} minutes from \var{b}), and then doing
     \class{datetime} subtraction, except that the implementation never
     overflows.
 
   \item
-    Comparison of \class{datetimetz} to \class{datetime} or datetimetz.  As for
-    subtraction, comparison is defined only if both operands are
-    \naive\ or both are aware.  If both are \naive, comparison is as
-    for \class{datetime} objects with the same date and time components.
-    If both are aware, comparison acts as if both were converted to
-    UTC datetimes first, except the the implementation never
-    overflows.  If one comparand is \naive\ and the other aware,
-    \exception{TypeError} is raised.
+    comparison of \class{datetimetz} to \class{datetime} or
+    \class{datetimetz}, where \var{a} is considered less than \var{b}
+    when \var{a} precedes \var{b} in time.  If one comparand is naive and
+    the other is aware, \exception{TypeError} is raised.  If both
+    comparands are aware, and have the same \member{tzinfo} member,
+    the common \member{tzinfo} member is ignored and the base datetimes
+    are compared.  If both comparands are aware and have different
+    \member{tzinfo} members, the comparands are first adjusted by
+    subtracting their UTC offsets (obtained from \code{self.utcoffset()}).
 
   \item
     hash, use as dict key
index a65e41cb076c6a1e1edd29e1a92a5f3a61d91113..dc58972be792de105ffb56c8823bcccbfdc6c088 100644 (file)
@@ -1657,9 +1657,8 @@ class TZInfoBase(unittest.TestCase):
     def test_aware_compare(self):
         cls = self.theclass
 
-        # Primarily trying to ensure that utcoffset() gets called even if
-        # the comparands have the same tzinfo member.  timetz comparison
-        # didn't used to do so, although datetimetz comparison did.
+        # Ensure that utcoffset() gets ignored if the comparands have
+        # the same tzinfo member.
         class OperandDependentOffset(tzinfo):
             def utcoffset(self, t):
                 if t.minute < 10:
@@ -1671,6 +1670,16 @@ class TZInfoBase(unittest.TestCase):
         d0 = base.replace(minute=3)
         d1 = base.replace(minute=9)
         d2 = base.replace(minute=11)
+        for x in d0, d1, d2:
+            for y in d0, d1, d2:
+                got = cmp(x, y)
+                expected = cmp(x.minute, y.minute)
+                self.assertEqual(got, expected)
+
+        # However, if they're different members, uctoffset is not ignored.
+        d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
+        d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
+        d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
         for x in d0, d1, d2:
             for y in d0, d1, d2:
                 got = cmp(x, y)
@@ -1893,6 +1902,36 @@ class TestTimeTZ(TestTime, TZInfoBase):
         self.assertRaises(ValueError, base.replace, second=100)
         self.assertRaises(ValueError, base.replace, microsecond=1000000)
 
+    def test_mixed_compare(self):
+        t1 = time(1, 2, 3)
+        t2 = timetz(1, 2, 3)
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=None)
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=FixedOffset(None, ""))
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=FixedOffset(0, ""))
+        self.assertRaises(TypeError, lambda: t1 == t2)
+
+        # In timetz w/ identical tzinfo objects, utcoffset is ignored.
+        class Varies(tzinfo):
+            def __init__(self):
+                self.offset = 22
+            def utcoffset(self, t):
+                self.offset += 1
+                return self.offset
+
+        v = Varies()
+        t1 = t2.replace(tzinfo=v)
+        t2 = t2.replace(tzinfo=v)
+        self.assertEqual(t1.utcoffset(), timedelta(minutes=23))
+        self.assertEqual(t2.utcoffset(), timedelta(minutes=24))
+        self.assertEqual(t1, t2)
+
+        # But if they're not identical, it isn't ignored.
+        t2 = t2.replace(tzinfo=Varies())
+        self.failUnless(t1 < t2)  # t1's offset counter still going up
+
 
 class TestDateTimeTZ(TestDateTime, TZInfoBase):
     theclass = datetimetz
@@ -1971,7 +2010,7 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
             def utcoffset(self, dt): return 1440 # out of bounds
         t1 = self.theclass(2, 2, 2, tzinfo=Bogus())
         t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, ""))
-        self.assertRaises(ValueError, lambda: t1 == t1)
+        self.assertRaises(ValueError, lambda: t1 == t2)
 
     def test_pickling(self):
         import pickle, cPickle
@@ -2389,10 +2428,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
     def test_aware_subtract(self):
         cls = self.theclass
 
-        # Primarily trying to ensure that utcoffset() gets called even if
-        # the operands have the same tzinfo member.  Subtraction didn't
-        # used to do this, and it makes a difference for DST-aware tzinfo
-        # instances.
+        # Ensure that utcoffset() is ignored when the operands have the
+        # same tzinfo member.
         class OperandDependentOffset(tzinfo):
             def utcoffset(self, t):
                 if t.minute < 10:
@@ -2404,6 +2441,18 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
         d0 = base.replace(minute=3)
         d1 = base.replace(minute=9)
         d2 = base.replace(minute=11)
+        for x in d0, d1, d2:
+            for y in d0, d1, d2:
+                got = x - y
+                expected = timedelta(minutes=x.minute - y.minute)
+                self.assertEqual(got, expected)
+
+        # OTOH, if the tzinfo members are distinct, utcoffsets aren't
+        # ignored.
+        base = cls(8, 9, 10, 11, 12, 13, 14)
+        d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
+        d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
+        d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
         for x in d0, d1, d2:
             for y in d0, d1, d2:
                 got = x - y
@@ -2418,6 +2467,35 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
                     expected = timedelta(minutes=0-(11-59))
                 self.assertEqual(got, expected)
 
+    def test_mixed_compare(self):
+        t1 = datetime(1, 2, 3, 4, 5, 6, 7)
+        t2 = datetimetz(1, 2, 3, 4, 5, 6, 7)
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=None)
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=FixedOffset(None, ""))
+        self.assertEqual(t1, t2)
+        t2 = t2.replace(tzinfo=FixedOffset(0, ""))
+        self.assertRaises(TypeError, lambda: t1 == t2)
+
+        # In datetimetz w/ identical tzinfo objects, utcoffset is ignored.
+        class Varies(tzinfo):
+            def __init__(self):
+                self.offset = 22
+            def utcoffset(self, t):
+                self.offset += 1
+                return self.offset
+
+        v = Varies()
+        t1 = t2.replace(tzinfo=v)
+        t2 = t2.replace(tzinfo=v)
+        self.assertEqual(t1.utcoffset(), timedelta(minutes=23))
+        self.assertEqual(t2.utcoffset(), timedelta(minutes=24))
+        self.assertEqual(t1, t2)
+
+        # But if they're not identical, it isn't ignored.
+        t2 = t2.replace(tzinfo=Varies())
+        self.failUnless(t1 < t2)  # t1's offset counter still going up
 
 def test_suite():
     allsuites = [unittest.makeSuite(klass, 'test')
index d7c6005acd1dfb816023c4ff1df84d18fe99f124..e460deed3f10c5566f3d712e899bb9cf4fc722fc 100644 (file)
@@ -3136,17 +3136,28 @@ datetime_richcompare(PyDateTime_DateTime *self, PyObject *other, int op)
                             other->ob_type->tp_name);
                return NULL;
        }
-       n1 = classify_utcoffset((PyObject *)self, &offset1);
-       assert(n1 != OFFSET_UNKNOWN);
-       if (n1 == OFFSET_ERROR)
-               return NULL;
-
-       n2 = classify_utcoffset(other, &offset2);
-       assert(n2 != OFFSET_UNKNOWN);
-       if (n2 == OFFSET_ERROR)
-               return NULL;
+       /* Ignore utcoffsets if they have identical tzinfo members.  This
+        * isn't an optimization, it's design.  If utcoffset() doesn't ignore
+        * its argument, it may return different results for self and other
+        * even if they have identical tzinfo members, and we're deliberately
+        * suppressing that (possible) difference.
+        */
+       if (get_tzinfo_member((PyObject *)self) == get_tzinfo_member(other)) {
+               offset1 = offset2 = 0;
+               n1 = n2 = OFFSET_NAIVE;
+       }
+       else {
+               n1 = classify_utcoffset((PyObject *)self, &offset1);
+               assert(n1 != OFFSET_UNKNOWN);
+               if (n1 == OFFSET_ERROR)
+                       return NULL;
 
-       /* If they're both naive, or both aware and have the same offsets,
+               n2 = classify_utcoffset(other, &offset2);
+               assert(n2 != OFFSET_UNKNOWN);
+               if (n2 == OFFSET_ERROR)
+                       return NULL;
+       }
+       /* If they're both naive, or both aware and have the same offsets,
         * we get off cheap.  Note that if they're both naive, offset1 ==
         * offset2 == 0 at this point.
         */
@@ -3656,15 +3667,27 @@ time_richcompare(PyDateTime_Time *self, PyObject *other, int op)
                             other->ob_type->tp_name);
                return NULL;
        }
-       n1 = classify_utcoffset((PyObject *)self, &offset1);
-       assert(n1 != OFFSET_UNKNOWN);
-       if (n1 == OFFSET_ERROR)
-               return NULL;
+       /* Ignore utcoffsets if they have identical tzinfo members.  This
+        * isn't an optimization, it's design.  If utcoffset() doesn't ignore
+        * its argument, it may return different results for self and other
+        * even if they have identical tzinfo members, and we're deliberately
+        * suppressing that (possible) difference.
+        */
+       if (get_tzinfo_member((PyObject *)self) == get_tzinfo_member(other)) {
+               offset1 = offset2 = 0;
+               n1 = n2 = OFFSET_NAIVE;
+       }
+       else {
+               n1 = classify_utcoffset((PyObject *)self, &offset1);
+               assert(n1 != OFFSET_UNKNOWN);
+               if (n1 == OFFSET_ERROR)
+                       return NULL;
 
-       n2 = classify_utcoffset(other, &offset2);
-       assert(n2 != OFFSET_UNKNOWN);
-       if (n2 == OFFSET_ERROR)
-               return NULL;
+               n2 = classify_utcoffset(other, &offset2);
+               assert(n2 != OFFSET_UNKNOWN);
+               if (n2 == OFFSET_ERROR)
+                       return NULL;
+       }
 
        /* If they're both naive, or both aware and have the same offsets,
         * we get off cheap.  Note that if they're both naive, offset1 ==
@@ -4601,15 +4624,29 @@ datetimetz_subtract(PyObject *left, PyObject *right)
                        int offset1, offset2;
                        PyDateTime_Delta *delta;
 
-                       n1 = classify_utcoffset(left, &offset1);
-                       assert(n1 != OFFSET_UNKNOWN);
-                       if (n1 == OFFSET_ERROR)
-                               return NULL;
-
-                       n2 = classify_utcoffset(right, &offset2);
-                       assert(n2 != OFFSET_UNKNOWN);
-                       if (n2 == OFFSET_ERROR)
-                               return NULL;
+                       /* Ignore utcoffsets if they have identical tzinfo
+                        * members.  This isn't an optimization, it's design.
+                        * If utcoffset() doesn't ignore its argument, it may
+                        * return different results for self and other even
+                        * if they have identical tzinfo members, and we're
+                        * deliberately suppressing that (possible) difference.
+                        */
+                       if (get_tzinfo_member(left) ==
+                           get_tzinfo_member(right)) {
+                               offset1 = offset2 = 0;
+                               n1 = n2 = OFFSET_NAIVE;
+                       }
+                       else {
+                               n1 = classify_utcoffset(left, &offset1);
+                               assert(n1 != OFFSET_UNKNOWN);
+                               if (n1 == OFFSET_ERROR)
+                                       return NULL;
+
+                               n2 = classify_utcoffset(right, &offset2);
+                               assert(n2 != OFFSET_UNKNOWN);
+                               if (n2 == OFFSET_ERROR)
+                                       return NULL;
+                       }
 
                        if (n1 != n2) {
                                PyErr_SetString(PyExc_TypeError,