Skip to content

Commit 5d24802

Browse files
gh-99772: Fix datetime.time comparison and hashing for sub-minute UTC offsets (#155024)
Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent 326f7f7 commit 5d24802

4 files changed

Lines changed: 65 additions & 28 deletions

File tree

Lib/_pydatetime.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,10 +1570,11 @@ def _cmp(self, other, allow_mixed=False):
15701570
return 2 # arbitrary non-zero value
15711571
else:
15721572
raise TypeError("cannot compare naive and aware times")
1573-
myhhmm = self._hour * 60 + self._minute - myoff//timedelta(minutes=1)
1574-
othhmm = other._hour * 60 + other._minute - otoff//timedelta(minutes=1)
1575-
return _cmp((myhhmm, self._second, self._microsecond),
1576-
(othhmm, other._second, other._microsecond))
1573+
myus = (((self._hour * 60 + self._minute) * 60 + self._second) * 1000000
1574+
+ self._microsecond - myoff._to_microseconds())
1575+
otus = (((other._hour * 60 + other._minute) * 60 + other._second) * 1000000
1576+
+ other._microsecond - otoff._to_microseconds())
1577+
return _cmp(myus, otus)
15771578

15781579
def __hash__(self):
15791580
"""Hash."""
@@ -1583,17 +1584,13 @@ def __hash__(self):
15831584
else:
15841585
t = self
15851586
tzoff = t.utcoffset()
1586-
if not tzoff: # zero or None
1587+
if tzoff is None:
15871588
self._hashcode = hash(t._getstate()[0])
15881589
else:
1589-
h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff,
1590-
timedelta(hours=1))
1591-
assert not m % timedelta(minutes=1), "whole minute"
1592-
m //= timedelta(minutes=1)
1593-
if 0 <= h < 24:
1594-
self._hashcode = hash(time(h, m, self.second, self.microsecond))
1595-
else:
1596-
self._hashcode = hash((h, m, self.second, self.microsecond))
1590+
self._hashcode = hash(timedelta(hours=t.hour,
1591+
minutes=t.minute,
1592+
seconds=t.second,
1593+
microseconds=t.microsecond) - tzoff)
15971594
return self._hashcode
15981595

15991596
# Conversion to string

Lib/test/datetimetester.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4693,6 +4693,33 @@ def tzname(self, dt): return self.tz
46934693
Badtzname.tz = '\ud800'
46944694
self.assertEqual(t.strftime("%Z"), '\ud800')
46954695

4696+
def test_subminute_offset_equality(self):
4697+
t1 = self.theclass(12, tzinfo=timezone.utc)
4698+
t2 = self.theclass(12, 0, 1, tzinfo=timezone(timedelta(seconds=1)))
4699+
self.assertEqual(t1, t2)
4700+
t2 = self.theclass(12, 0, 0, 1, tzinfo=timezone(timedelta(microseconds=1)))
4701+
self.assertEqual(t1, t2)
4702+
t2 = self.theclass(11, 59, 59, 999999, tzinfo=timezone(timedelta(microseconds=-1)))
4703+
self.assertEqual(t1, t2)
4704+
4705+
def test_subminute_offset_ordering(self):
4706+
t1 = self.theclass(0, tzinfo=timezone.utc)
4707+
t2 = self.theclass(0, tzinfo=timezone(timedelta(microseconds=1)))
4708+
self.assertGreater(t1, t2)
4709+
4710+
t1 = self.theclass(13, 59, 59, 900000, tzinfo=timezone(timedelta(hours=2)))
4711+
t2 = self.theclass(14, tzinfo=timezone(timedelta(hours=2, microseconds=900000)))
4712+
self.assertGreater(t1, t2)
4713+
4714+
def test_subminute_offset_hash(self):
4715+
t1 = self.theclass(12, tzinfo=timezone.utc)
4716+
t2 = self.theclass(12, 0, 1, tzinfo=timezone(timedelta(seconds=1)))
4717+
self.assertEqual(hash(t1), hash(t2))
4718+
t2 = self.theclass(12, 0, 0, 1, tzinfo=timezone(timedelta(microseconds=1)))
4719+
self.assertEqual(hash(t1), hash(t2))
4720+
t2 = self.theclass(11, 59, 59, 999999, tzinfo=timezone(timedelta(microseconds=-1)))
4721+
self.assertEqual(hash(t1), hash(t2))
4722+
46964723
def test_hash_edge_cases(self):
46974724
# Offsets that overflow a basic time.
46984725
t1 = self.theclass(0, 1, 2, 3, tzinfo=FixedOffset(1439, ""))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix comparisons and hashing of :class:`datetime.time` objects with sub-minute
2+
UTC offsets.

Modules/_datetimemodule.c

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5051,22 +5051,33 @@ time_richcompare(PyObject *self, PyObject *other, int op)
50515051
}
50525052
/* The hard case: both aware with different UTC offsets */
50535053
else if (offset1 != Py_None && offset2 != Py_None) {
5054-
int offsecs1, offsecs2;
5054+
long long norm_us1, norm_us2;
50555055
assert(offset1 != offset2); /* else last "if" handled it */
5056-
offsecs1 = TIME_GET_HOUR(self) * 3600 +
5057-
TIME_GET_MINUTE(self) * 60 +
5058-
TIME_GET_SECOND(self) -
5059-
GET_TD_DAYS(offset1) * 86400 -
5060-
GET_TD_SECONDS(offset1);
5061-
offsecs2 = TIME_GET_HOUR(other) * 3600 +
5062-
TIME_GET_MINUTE(other) * 60 +
5063-
TIME_GET_SECOND(other) -
5064-
GET_TD_DAYS(offset2) * 86400 -
5065-
GET_TD_SECONDS(offset2);
5066-
diff = offsecs1 - offsecs2;
5067-
if (diff == 0)
5068-
diff = TIME_GET_MICROSECOND(self) -
5069-
TIME_GET_MICROSECOND(other);
5056+
norm_us1 =
5057+
((TIME_GET_HOUR(self) * 3600 +
5058+
TIME_GET_MINUTE(self) * 60 +
5059+
TIME_GET_SECOND(self)) * 1000000LL +
5060+
TIME_GET_MICROSECOND(self)) -
5061+
((GET_TD_DAYS(offset1) * 86400LL +
5062+
GET_TD_SECONDS(offset1)) * 1000000LL +
5063+
GET_TD_MICROSECONDS(offset1));
5064+
norm_us2 =
5065+
((TIME_GET_HOUR(other) * 3600 +
5066+
TIME_GET_MINUTE(other) * 60 +
5067+
TIME_GET_SECOND(other)) * 1000000LL +
5068+
TIME_GET_MICROSECOND(other)) -
5069+
((GET_TD_DAYS(offset2) * 86400LL +
5070+
GET_TD_SECONDS(offset2)) * 1000000LL +
5071+
GET_TD_MICROSECONDS(offset2));
5072+
if (norm_us1 < norm_us2) {
5073+
diff = -1;
5074+
}
5075+
else if (norm_us1 > norm_us2) {
5076+
diff = 1;
5077+
}
5078+
else {
5079+
diff = 0;
5080+
}
50705081
result = diff_to_bool(diff, op);
50715082
}
50725083
else if (op == Py_EQ) {

0 commit comments

Comments
 (0)