diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 55570341cf4e8..247f2a7515705 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -49,6 +49,26 @@ For example: buffer = io.BytesIO() data.to_csv(buffer, mode="w+b", encoding="utf-8", compression="gzip") +Arithmetic with Timestamp and Timedelta-based Intervals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Arithmetic can now be performed on :class:`Interval` s having their left and right +ends as :class:`Timestamp` s or :class:`Timedelta` s, like what would be possible +if the ends were numeric (:issue:`35908`). + +Arithmetic can be performed by directly using arithmetic operators (`-` or `+`), +so something like this will work: + +.. ipython:: python + + interval = pd.Interval(pd.Timestamp("1900-01-01"), pd.Timestamp("1900-01-02")) + interval - pd.Timestamp("1900-01-01") + +This works when endpoints are :class:`Timestamp` s or :class:`Timedelta` s. + +However, it should be noted that adding :class:`Timestamp` s , and subtracting :class:`Timestamp` +from a :class:`Timedelta` is illegal. + .. _whatsnew_120.enhancements.other: Other enhancements diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 6867e8aba7411..393fecf1259bc 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -395,6 +395,8 @@ cdef class Interval(IntervalMixin): isinstance(y, numbers.Number) or PyDelta_Check(y) or is_timedelta64_object(y) + or isinstance(y, _Timestamp) + or isinstance(y, _Timedelta) ): return Interval(self.left + y, self.right + y, closed=self.closed) elif ( @@ -413,6 +415,8 @@ cdef class Interval(IntervalMixin): isinstance(y, numbers.Number) or PyDelta_Check(y) or is_timedelta64_object(y) + or isinstance(y, _Timestamp) + or isinstance(y, _Timedelta) ): return Interval(self.left - y, self.right - y, closed=self.closed) return NotImplemented diff --git a/pandas/tests/scalar/interval/test_interval.py b/pandas/tests/scalar/interval/test_interval.py index a0151bb9ac7bf..5fd3d34a84f20 100644 --- a/pandas/tests/scalar/interval/test_interval.py +++ b/pandas/tests/scalar/interval/test_interval.py @@ -184,6 +184,184 @@ def test_math_sub(self, closed): with pytest.raises(TypeError, match=msg): interval - "foo" + def test_math_sub_interval_timestamp_timestamp(self, closed): + # Tests for interval of timestamp - timestamp + interval = Interval( + Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed + ) + expected = Interval( + Timedelta("0 days 00:00:00"), Timedelta("1 days 00:00:00"), closed=closed + ) + + result = interval - Timestamp("1900-01-01") + assert result == expected + + expected = Interval( + interval.left - Timestamp("1900-01-01"), + interval.right - Timestamp("1900-01-01"), + closed=closed, + ) + assert result == expected + + result = interval + result -= Timestamp("1900-01-01") + + expected = Interval( + Timedelta("0 days 00:00:00"), Timedelta("1 days 00:00:00"), closed=closed + ) + assert result == expected + + expected = Interval( + interval.left - Timestamp("1900-01-01"), + interval.right - Timestamp("1900-01-01"), + closed=closed, + ) + assert result == expected + + def test_math_sub_interval_timestamp_timedelta(self, closed): + # Tests for interval of timestamps - timedelta + interval = Interval( + Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed + ) + expected = Interval( + Timestamp("1899-12-31"), Timestamp("1900-01-01"), closed=closed + ) + + result = interval - Timedelta("1 days 00:00:00") + assert result == expected + + expected = Interval( + interval.left - Timedelta("1 days 00:00:00"), + interval.right - Timedelta("1 days 00:00:00"), + closed=closed, + ) + assert result == expected + + result = interval + result -= Timedelta("1 days 00:00:00") + + expected = Interval( + Timestamp("1899-12-31"), Timestamp("1900-01-01"), closed=closed + ) + assert result == expected + + expected = Interval( + interval.left - Timedelta("1 days 00:00:00"), + interval.right - Timedelta("1 days 00:00:00"), + closed=closed, + ) + assert result == expected + + def test_math_add_interval_timestamp_timedelta(self, closed): + interval = Interval( + Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed + ) + expected = Interval( + Timestamp("1900-01-02"), Timestamp("1900-01-03"), closed=closed + ) + + result = interval + Timedelta("1 days 00:00:00") + assert result == expected + + result = interval + result += Timedelta("1 days 00:00:00") + assert result == expected + + expected = Interval( + interval.left + Timedelta("1 days 00:00:00"), + interval.right + Timedelta("1 days 00:00:00"), + closed=closed, + ) + + result = interval + Timedelta("1 days 00:00:00") + assert result == expected + + result = interval + result += Timedelta("1 days 00:00:00") + assert result == expected + + def test_math_add_interval_timedelta_timedelta(self, closed): + interval = Interval( + Timedelta("1 days 00:00:00"), Timedelta("2 days 00:00:00"), closed=closed + ) + expected = Interval( + Timedelta("4 days 01:00:00"), Timedelta("5 days 01:00:00"), closed=closed + ) + + result = interval + Timedelta("3 days 01:00:00") + assert result == expected + + result = interval + result += Timedelta("3 days 01:00:00") + assert result == expected + + expected = Interval( + interval.left + Timedelta("3 days 01:00:00"), + interval.right + Timedelta("3 days 01:00:00"), + closed=closed, + ) + + result = interval + Timedelta("3 days 01:00:00") + assert result == expected + + result = interval + result += Timedelta("3 days 01:00:00") + assert result == expected + + def test_sub_interval_imedelta_timedelta(self, closed): + interval = Interval( + Timedelta("1 days 00:00:00"), Timedelta("2 days 00:00:00"), closed=closed + ) + expected = Interval( + Timedelta("-3 days +23:00:00"), + Timedelta("-2 days +23:00:00"), + closed=closed, + ) + + result = interval - Timedelta("3 days 01:00:00") + assert result == expected + + result = interval + result -= Timedelta("3 days 01:00:00") + assert result == expected + + expected = Interval( + interval.left - Timedelta("3 days 01:00:00"), + interval.right - Timedelta("3 days 01:00:00"), + closed=closed, + ) + + result = interval - Timedelta("3 days 01:00:00") + assert result == expected + + result = interval + result -= Timedelta("3 days 01:00:00") + assert result == expected + + def test_math_add_interval_timestamp_timestamp(self, closed): + interval = Interval( + Timestamp("1900-01-01"), Timestamp("1900-01-02"), closed=closed + ) + + msg = r"unsupported operand type\(s\) for \+" + with pytest.raises(TypeError, match=msg): + interval = interval + Timestamp("2002-01-08") + + with pytest.raises(TypeError, match=msg): + interval += Timestamp("2002-01-08") + + def test_math_sub_interval_timedelta_timestamp(self, closed): + interval = Interval( + Timedelta("1 days 00:00:00"), Timedelta("3 days 00:00:00"), closed=closed + ) + + msg = r"unsupported operand type\(s\) for \-" + with pytest.raises(TypeError, match=msg): + interval = interval - Timestamp("1900-01-01") + + with pytest.raises(TypeError, match=msg): + interval -= Timestamp("1900-01-01") + def test_math_mult(self, closed): interval = Interval(0, 1, closed=closed) expected = Interval(0, 2, closed=closed)