Skip to content

Commit

Permalink
Backport PR #51538 on branch 2.0.x (BUG: Timedelta comparisons with v…
Browse files Browse the repository at this point in the history
…ery large pytimedeltas overflowing) (#52241)

Backport PR #51538: BUG: Timedelta comparisons with very large pytimedeltas overflowing

Co-authored-by: jbrockmendel <jbrockmendel@gmail.com>
  • Loading branch information
meeseeksmachine and jbrockmendel committed Mar 27, 2023
1 parent 2c21af5 commit 4fdf0a6
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 2 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ Timedelta
- Bug in :func:`to_timedelta` raising error when input has nullable dtype ``Float64`` (:issue:`48796`)
- Bug in :class:`Timedelta` constructor incorrectly raising instead of returning ``NaT`` when given a ``np.timedelta64("nat")`` (:issue:`48898`)
- Bug in :class:`Timedelta` constructor failing to raise when passed both a :class:`Timedelta` object and keywords (e.g. days, seconds) (:issue:`48898`)
- Bug in :class:`Timedelta` comparisons with very large ``datetime.timedelta`` objects incorrect raising ``OutOfBoundsTimedelta`` (:issue:`49021`)

Timezones
^^^^^^^^^
Expand Down
27 changes: 25 additions & 2 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import warnings
cimport cython
from cpython.object cimport (
Py_EQ,
Py_GE,
Py_GT,
Py_LE,
Py_LT,
Py_NE,
PyObject,
PyObject_RichCompare,
Expand Down Expand Up @@ -1150,8 +1154,27 @@ cdef class _Timedelta(timedelta):
if isinstance(other, _Timedelta):
ots = other
elif is_any_td_scalar(other):
ots = Timedelta(other)
# TODO: watch out for overflows
try:
ots = Timedelta(other)
except OutOfBoundsTimedelta as err:
# GH#49021 pytimedelta.max overflows
if not PyDelta_Check(other):
# TODO: handle this case
raise
ltup = (self.days, self.seconds, self.microseconds, self.nanoseconds)
rtup = (other.days, other.seconds, other.microseconds, 0)
if op == Py_EQ:
return ltup == rtup
elif op == Py_NE:
return ltup != rtup
elif op == Py_LT:
return ltup < rtup
elif op == Py_LE:
return ltup <= rtup
elif op == Py_GT:
return ltup > rtup
elif op == Py_GE:
return ltup >= rtup

elif other is NaT:
return op == Py_NE
Expand Down
64 changes: 64 additions & 0 deletions pandas/tests/scalar/timedelta/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,70 @@ def test_td_op_timedelta_timedeltalike_array(self, op, arr):


class TestTimedeltaComparison:
def test_compare_pytimedelta_bounds(self):
# GH#49021 don't overflow on comparison with very large pytimedeltas

for unit in ["ns", "us"]:
tdmax = Timedelta.max.as_unit(unit).max
tdmin = Timedelta.min.as_unit(unit).min

assert tdmax < timedelta.max
assert tdmax <= timedelta.max
assert not tdmax > timedelta.max
assert not tdmax >= timedelta.max
assert tdmax != timedelta.max
assert not tdmax == timedelta.max

assert tdmin > timedelta.min
assert tdmin >= timedelta.min
assert not tdmin < timedelta.min
assert not tdmin <= timedelta.min
assert tdmin != timedelta.min
assert not tdmin == timedelta.min

# But the "ms" and "s"-reso bounds extend pass pytimedelta
for unit in ["ms", "s"]:
tdmax = Timedelta.max.as_unit(unit).max
tdmin = Timedelta.min.as_unit(unit).min

assert tdmax > timedelta.max
assert tdmax >= timedelta.max
assert not tdmax < timedelta.max
assert not tdmax <= timedelta.max
assert tdmax != timedelta.max
assert not tdmax == timedelta.max

assert tdmin < timedelta.min
assert tdmin <= timedelta.min
assert not tdmin > timedelta.min
assert not tdmin >= timedelta.min
assert tdmin != timedelta.min
assert not tdmin == timedelta.min

def test_compare_pytimedelta_bounds2(self):
# a pytimedelta outside the microsecond bounds
pytd = timedelta(days=999999999, seconds=86399)
# NB: np.timedelta64(td, "s"") incorrectly overflows
td64 = np.timedelta64(pytd.days, "D") + np.timedelta64(pytd.seconds, "s")
td = Timedelta(td64)
assert td.days == pytd.days
assert td.seconds == pytd.seconds

assert td == pytd
assert not td != pytd
assert not td < pytd
assert not td > pytd
assert td <= pytd
assert td >= pytd

td2 = td - Timedelta(seconds=1).as_unit("s")
assert td2 != pytd
assert not td2 == pytd
assert td2 < pytd
assert td2 <= pytd
assert not td2 > pytd
assert not td2 >= pytd

def test_compare_tick(self, tick_classes):
cls = tick_classes

Expand Down

0 comments on commit 4fdf0a6

Please sign in to comment.