From 60c76e4016f32617913fbdeae0c867c40634eac1 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Fri, 27 Dec 2002 00:41:11 +0000 Subject: [PATCH] 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. --- Doc/lib/libdatetime.tex | 52 ++++++++++++---------- Lib/test/test_datetime.py | 94 +++++++++++++++++++++++++++++++++++---- Modules/datetimemodule.c | 89 +++++++++++++++++++++++++----------- 3 files changed, 178 insertions(+), 57 deletions(-) diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex index 1adbc8e863c..32c0f93abbc 100644 --- a/Doc/lib/libdatetime.tex +++ b/Doc/lib/libdatetime.tex @@ -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 diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index a65e41cb076..dc58972be79 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -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') diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index d7c6005acd1..e460deed3f1 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -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,