Skip to content

Commit 125e1ea

Browse files
authored
BUG: Timedelta comparisons with very large pytimedeltas overflowing (#51538)
* BUG: Timedelta comparisons with very large pytimedeltas overflowing * Handle missed cases
1 parent e63e2af commit 125e1ea

File tree

3 files changed

+90
-2
lines changed

3 files changed

+90
-2
lines changed

doc/source/whatsnew/v2.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,7 @@ Timedelta
11901190
- Bug in :func:`to_timedelta` raising error when input has nullable dtype ``Float64`` (:issue:`48796`)
11911191
- Bug in :class:`Timedelta` constructor incorrectly raising instead of returning ``NaT`` when given a ``np.timedelta64("nat")`` (:issue:`48898`)
11921192
- Bug in :class:`Timedelta` constructor failing to raise when passed both a :class:`Timedelta` object and keywords (e.g. days, seconds) (:issue:`48898`)
1193+
- Bug in :class:`Timedelta` comparisons with very large ``datetime.timedelta`` objects incorrect raising ``OutOfBoundsTimedelta`` (:issue:`49021`)
11931194

11941195
Timezones
11951196
^^^^^^^^^

pandas/_libs/tslibs/timedeltas.pyx

+25-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import warnings
44
cimport cython
55
from cpython.object cimport (
66
Py_EQ,
7+
Py_GE,
8+
Py_GT,
9+
Py_LE,
10+
Py_LT,
711
Py_NE,
812
PyObject,
913
PyObject_RichCompare,
@@ -1154,8 +1158,27 @@ cdef class _Timedelta(timedelta):
11541158
if isinstance(other, _Timedelta):
11551159
ots = other
11561160
elif is_any_td_scalar(other):
1157-
ots = Timedelta(other)
1158-
# TODO: watch out for overflows
1161+
try:
1162+
ots = Timedelta(other)
1163+
except OutOfBoundsTimedelta as err:
1164+
# GH#49021 pytimedelta.max overflows
1165+
if not PyDelta_Check(other):
1166+
# TODO: handle this case
1167+
raise
1168+
ltup = (self.days, self.seconds, self.microseconds, self.nanoseconds)
1169+
rtup = (other.days, other.seconds, other.microseconds, 0)
1170+
if op == Py_EQ:
1171+
return ltup == rtup
1172+
elif op == Py_NE:
1173+
return ltup != rtup
1174+
elif op == Py_LT:
1175+
return ltup < rtup
1176+
elif op == Py_LE:
1177+
return ltup <= rtup
1178+
elif op == Py_GT:
1179+
return ltup > rtup
1180+
elif op == Py_GE:
1181+
return ltup >= rtup
11591182

11601183
elif other is NaT:
11611184
return op == Py_NE

pandas/tests/scalar/timedelta/test_arithmetic.py

+64
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,70 @@ def test_td_op_timedelta_timedeltalike_array(self, op, arr):
966966

967967

968968
class TestTimedeltaComparison:
969+
def test_compare_pytimedelta_bounds(self):
970+
# GH#49021 don't overflow on comparison with very large pytimedeltas
971+
972+
for unit in ["ns", "us"]:
973+
tdmax = Timedelta.max.as_unit(unit).max
974+
tdmin = Timedelta.min.as_unit(unit).min
975+
976+
assert tdmax < timedelta.max
977+
assert tdmax <= timedelta.max
978+
assert not tdmax > timedelta.max
979+
assert not tdmax >= timedelta.max
980+
assert tdmax != timedelta.max
981+
assert not tdmax == timedelta.max
982+
983+
assert tdmin > timedelta.min
984+
assert tdmin >= timedelta.min
985+
assert not tdmin < timedelta.min
986+
assert not tdmin <= timedelta.min
987+
assert tdmin != timedelta.min
988+
assert not tdmin == timedelta.min
989+
990+
# But the "ms" and "s"-reso bounds extend pass pytimedelta
991+
for unit in ["ms", "s"]:
992+
tdmax = Timedelta.max.as_unit(unit).max
993+
tdmin = Timedelta.min.as_unit(unit).min
994+
995+
assert tdmax > timedelta.max
996+
assert tdmax >= timedelta.max
997+
assert not tdmax < timedelta.max
998+
assert not tdmax <= timedelta.max
999+
assert tdmax != timedelta.max
1000+
assert not tdmax == timedelta.max
1001+
1002+
assert tdmin < timedelta.min
1003+
assert tdmin <= timedelta.min
1004+
assert not tdmin > timedelta.min
1005+
assert not tdmin >= timedelta.min
1006+
assert tdmin != timedelta.min
1007+
assert not tdmin == timedelta.min
1008+
1009+
def test_compare_pytimedelta_bounds2(self):
1010+
# a pytimedelta outside the microsecond bounds
1011+
pytd = timedelta(days=999999999, seconds=86399)
1012+
# NB: np.timedelta64(td, "s"") incorrectly overflows
1013+
td64 = np.timedelta64(pytd.days, "D") + np.timedelta64(pytd.seconds, "s")
1014+
td = Timedelta(td64)
1015+
assert td.days == pytd.days
1016+
assert td.seconds == pytd.seconds
1017+
1018+
assert td == pytd
1019+
assert not td != pytd
1020+
assert not td < pytd
1021+
assert not td > pytd
1022+
assert td <= pytd
1023+
assert td >= pytd
1024+
1025+
td2 = td - Timedelta(seconds=1).as_unit("s")
1026+
assert td2 != pytd
1027+
assert not td2 == pytd
1028+
assert td2 < pytd
1029+
assert td2 <= pytd
1030+
assert not td2 > pytd
1031+
assert not td2 >= pytd
1032+
9691033
def test_compare_tick(self, tick_classes):
9701034
cls = tick_classes
9711035

0 commit comments

Comments
 (0)