Make comparison and subtraction of aware objects ignore tzinfo if the

operands have identical tzinfo members (meaning object identity -- "is").
I misunderstood the intent here, reading wrong conclusion into
conflicting clues.
This commit is contained in:
Tim Peters 2002-12-27 00:41:11 +00:00
parent f044e090c4
commit 60c76e4016
3 changed files with 178 additions and 57 deletions

View 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

View 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')

View 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;
/* 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;
/* 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;
/* 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;
n2 = classify_utcoffset(right, &offset2);
assert(n2 != OFFSET_UNKNOWN);
if (n2 == OFFSET_ERROR)
return NULL;
}
if (n1 != n2) {
PyErr_SetString(PyExc_TypeError,