diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index cd1cb0b64f74a..8a05d0d36cd2f 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -525,6 +525,7 @@ Indexing - Bug in :class:`Index` constructor where an unhelpful error message was raised for ``numpy`` scalars (:issue:`33017`) - Bug in :meth:`DataFrame.lookup` incorrectly raising an ``AttributeError`` when ``frame.index`` or ``frame.columns`` is not unique; this will now raise a ``ValueError`` with a helpful error message (:issue:`33041`) - Bug in :meth:`DataFrame.iloc.__setitem__` creating a new array instead of overwriting ``Categorical`` values in-place (:issue:`32831`) +- Bug in :class:`Interval` where a :class:`Timedelta` could not be added or subtracted from a :class:`Timestamp` interval (:issue:`32023`) - Bug in :meth:`DataFrame.copy` _item_cache not invalidated after copy causes post-copy value updates to not be reflected (:issue:`31784`) - Bug in `Series.__getitem__` with an integer key and a :class:`MultiIndex` with leading integer level failing to raise ``KeyError`` if the key is not present in the first level (:issue:`33355`) - Bug in :meth:`DataFrame.iloc` when slicing a single column-:class:`DataFrame`` with ``ExtensionDtype`` (e.g. ``df.iloc[:, :1]``) returning an invalid result (:issue:`32957`) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index a47303ddc93cf..e52474e233510 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -1,6 +1,9 @@ import numbers from operator import le, lt +from cpython.datetime cimport PyDelta_Check, PyDateTime_IMPORT +PyDateTime_IMPORT + from cpython.object cimport ( Py_EQ, Py_GE, @@ -11,7 +14,6 @@ from cpython.object cimport ( PyObject_RichCompare, ) - import cython from cython import Py_ssize_t @@ -34,7 +36,11 @@ cnp.import_array() cimport pandas._libs.util as util from pandas._libs.hashtable cimport Int64Vector -from pandas._libs.tslibs.util cimport is_integer_object, is_float_object +from pandas._libs.tslibs.util cimport ( + is_integer_object, + is_float_object, + is_timedelta64_object, +) from pandas._libs.tslibs import Timestamp from pandas._libs.tslibs.timedeltas import Timedelta @@ -294,6 +300,7 @@ cdef class Interval(IntervalMixin): True """ _typ = "interval" + __array_priority__ = 1000 cdef readonly object left """ @@ -398,14 +405,29 @@ cdef class Interval(IntervalMixin): return f'{start_symbol}{left}, {right}{end_symbol}' def __add__(self, y): - if isinstance(y, numbers.Number): + if ( + isinstance(y, numbers.Number) + or PyDelta_Check(y) + or is_timedelta64_object(y) + ): return Interval(self.left + y, self.right + y, closed=self.closed) - elif isinstance(y, Interval) and isinstance(self, numbers.Number): + elif ( + isinstance(y, Interval) + and ( + isinstance(self, numbers.Number) + or PyDelta_Check(self) + or is_timedelta64_object(self) + ) + ): return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented def __sub__(self, y): - if isinstance(y, numbers.Number): + if ( + isinstance(y, numbers.Number) + or PyDelta_Check(y) + or is_timedelta64_object(y) + ): return Interval(self.left - y, self.right - y, closed=self.closed) return NotImplemented diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py new file mode 100644 index 0000000000000..5252f1a4d5a24 --- /dev/null +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -0,0 +1,47 @@ +from datetime import timedelta + +import numpy as np +import pytest + +from pandas import Interval, Timedelta, Timestamp + + +@pytest.mark.parametrize("method", ["__add__", "__sub__"]) +@pytest.mark.parametrize( + "interval", + [ + Interval(Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00")), + Interval(Timedelta(days=7), Timedelta(days=14)), + ], +) +@pytest.mark.parametrize( + "delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")] +) +def test_time_interval_add_subtract_timedelta(interval, delta, method): + # https://github.com/pandas-dev/pandas/issues/32023 + result = getattr(interval, method)(delta) + left = getattr(interval.left, method)(delta) + right = getattr(interval.right, method)(delta) + expected = Interval(left, right) + + assert result == expected + + +@pytest.mark.parametrize("interval", [Interval(1, 2), Interval(1.0, 2.0)]) +@pytest.mark.parametrize( + "delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")] +) +def test_numeric_interval_add_timedelta_raises(interval, delta): + # https://github.com/pandas-dev/pandas/issues/32023 + msg = "|".join( + [ + "unsupported operand", + "cannot use operands", + "Only numeric, Timestamp and Timedelta endpoints are allowed", + ] + ) + with pytest.raises((TypeError, ValueError), match=msg): + interval + delta + + with pytest.raises((TypeError, ValueError), match=msg): + delta + interval