From 95f2fbea7fab6ac1491d0ad7dc558ec2a3b5fc55 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Tue, 18 Feb 2020 22:15:26 -0600 Subject: [PATCH 01/16] Add test --- pandas/tests/arithmetic/test_interval.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index f9e1a515277d5..bd1000252ed6e 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -271,3 +271,21 @@ def test_index_series_compat(self, op, constructor, expected_type, assert_func): result = op(index, other) expected = expected_type(self.elementwise_comparison(op, index, other)) assert_func(result, expected) + + +@pytest.mark.parametrize("add", [True, False]) +def test_timestamp_interval_can_add_timedelta(add): + # https://github.com/pandas-dev/pandas/issues/32023 + interval = pd.Interval( + pd.Timestamp("2017-01-01 00:00:00"), pd.Timestamp("2018-01-01 00:00:00") + ) + delta = pd.Timedelta(days=7) + + if add: + result = interval + delta + expected = pd.Interval(interval.left + delta, interval.right + delta) + else: + result = interval - delta + expected = pd.Interval(interval.left - delta, interval.right - delta) + + assert result == expected From 7438110392351329b37007b7ec4d69b9f0e823fa Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Tue, 18 Feb 2020 22:15:43 -0600 Subject: [PATCH 02/16] Check for Timedelta --- pandas/_libs/interval.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 1166768472449..8a3a8c46ccddd 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -383,14 +383,14 @@ 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, Timedelta)): return Interval(self.left + y, self.right + y, closed=self.closed) elif isinstance(y, Interval) and isinstance(self, numbers.Number): 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, Timedelta)): return Interval(self.left - y, self.right - y, closed=self.closed) return NotImplemented From 4e2c07a88ef65b10133418041de7b2b6aa0e2cea Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 19 Feb 2020 08:42:18 -0600 Subject: [PATCH 03/16] Update test --- pandas/tests/arithmetic/test_interval.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index bd1000252ed6e..1613dfe89bf87 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -273,19 +273,16 @@ def test_index_series_compat(self, op, constructor, expected_type, assert_func): assert_func(result, expected) -@pytest.mark.parametrize("add", [True, False]) -def test_timestamp_interval_can_add_timedelta(add): +@pytest.mark.parametrize("method", ["__add__", "__sub__"]) +def test_timestamp_interval_add_subtract_timedelta(method): # https://github.com/pandas-dev/pandas/issues/32023 - interval = pd.Interval( - pd.Timestamp("2017-01-01 00:00:00"), pd.Timestamp("2018-01-01 00:00:00") + interval = Interval( + Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") ) - delta = pd.Timedelta(days=7) - - if add: - result = interval + delta - expected = pd.Interval(interval.left + delta, interval.right + delta) - else: - result = interval - delta - expected = pd.Interval(interval.left - delta, interval.right - delta) + delta = Timedelta(days=7) + result = getattr(interval, method)(delta) + left = getattr(interval.left, method)(delta) + right = getattr(interval.right, method)(delta) + expected = Interval(left, right) assert result == expected From 5b3074074baac775c8ff9c1937786f485b464b3c Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 19 Feb 2020 08:43:09 -0600 Subject: [PATCH 04/16] whatsnew --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 13827e8fc4c33..7079eb2ed11a0 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -149,7 +149,7 @@ Strings Interval ^^^^^^^^ -- +- Fixed bug in :class:`Interval` where a :class:`Timedelta` could not be added or subtracted from a :class:`Timestamp` interval (:issue:`32023`) - Indexing From d88aca0533c161bb3dee418d17cce5ef0f7e6cb8 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 19 Feb 2020 08:45:21 -0600 Subject: [PATCH 05/16] Remove unreachable --- pandas/_libs/interval.pyx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 8a3a8c46ccddd..52b6270a439c1 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -385,8 +385,6 @@ cdef class Interval(IntervalMixin): def __add__(self, y): if isinstance(y, (numbers.Number, Timedelta)): return Interval(self.left + y, self.right + y, closed=self.closed) - elif isinstance(y, Interval) and isinstance(self, numbers.Number): - return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented def __sub__(self, y): @@ -397,8 +395,6 @@ cdef class Interval(IntervalMixin): def __mul__(self, y): if isinstance(y, numbers.Number): return Interval(self.left * y, self.right * y, closed=self.closed) - elif isinstance(y, Interval) and isinstance(self, numbers.Number): - return Interval(y.left * self, y.right * self, closed=y.closed) return NotImplemented def __div__(self, y): From 48e4794c429f48bba355b585add77157a2d39863 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 19 Feb 2020 08:55:34 -0600 Subject: [PATCH 06/16] Revert "Remove unreachable" This reverts commit d88aca0533c161bb3dee418d17cce5ef0f7e6cb8. --- pandas/_libs/interval.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 52b6270a439c1..8a3a8c46ccddd 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -385,6 +385,8 @@ cdef class Interval(IntervalMixin): def __add__(self, y): if isinstance(y, (numbers.Number, Timedelta)): return Interval(self.left + y, self.right + y, closed=self.closed) + elif isinstance(y, Interval) and isinstance(self, numbers.Number): + return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented def __sub__(self, y): @@ -395,6 +397,8 @@ cdef class Interval(IntervalMixin): def __mul__(self, y): if isinstance(y, numbers.Number): return Interval(self.left * y, self.right * y, closed=self.closed) + elif isinstance(y, Interval) and isinstance(self, numbers.Number): + return Interval(y.left * self, y.right * self, closed=y.closed) return NotImplemented def __div__(self, y): From fe10a032dc2542753811764c55a1a24746d37cc1 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 21 Feb 2020 10:37:49 -0600 Subject: [PATCH 07/16] Update tests --- pandas/tests/arithmetic/test_interval.py | 15 ------ .../tests/scalar/interval/test_arithmetic.py | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 pandas/tests/scalar/interval/test_arithmetic.py diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index 1613dfe89bf87..f9e1a515277d5 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -271,18 +271,3 @@ def test_index_series_compat(self, op, constructor, expected_type, assert_func): result = op(index, other) expected = expected_type(self.elementwise_comparison(op, index, other)) assert_func(result, expected) - - -@pytest.mark.parametrize("method", ["__add__", "__sub__"]) -def test_timestamp_interval_add_subtract_timedelta(method): - # https://github.com/pandas-dev/pandas/issues/32023 - interval = Interval( - Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") - ) - delta = Timedelta(days=7) - result = getattr(interval, method)(delta) - left = getattr(interval.left, method)(delta) - right = getattr(interval.right, method)(delta) - expected = Interval(left, right) - - assert result == expected diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py new file mode 100644 index 0000000000000..38d54da192e40 --- /dev/null +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -0,0 +1,52 @@ +from datetime import timedelta + +from numpy import timedelta64 +import pytest + +from pandas import Interval, Timedelta, Timestamp + + +@pytest.mark.parametrize("method", ["__add__", "__sub__"]) +@pytest.mark.parametrize( + "delta", [Timedelta(days=7), timedelta(7), timedelta64(7, "D")] +) +def test_timestamp_interval_add_subtract_timedelta(method, delta): + # https://github.com/pandas-dev/pandas/issues/32023 + interval = Interval( + Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") + ) + + result = getattr(interval, method)(delta) + + left = getattr(interval.left, method)(delta) + right = getattr(interval.right, method)(delta) + expected = Interval(left, right) + + assert result == expected + + +def test_timedelta_add_timestamp_interval(): + # https://github.com/pandas-dev/pandas/issues/32023 + interval = Interval( + Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") + ) + delta = Timedelta(days=7) + + result = delta + interval + + left = interval.left + delta + right = interval.right + delta + expected = Interval(left, right) + + assert result == expected + + +@pytest.mark.parametrize("method", ["__add__", "__sub__"]) +def test_numeric_interval_add_subtract_timedelta_raises(method): + # https://github.com/pandas-dev/pandas/issues/32023 + interval = Interval(1, 2) + delta = Timedelta(days=7) + + msg = "unsupported operand" + with pytest.raises(TypeError, match=msg): + getattr(interval, method)(delta) From b5f169f3032787cec708de1cc70259ff99f00e7a Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 21 Feb 2020 10:47:09 -0600 Subject: [PATCH 08/16] Tests --- pandas/tests/arithmetic/test_interval.py | 8 ++++++++ pandas/tests/scalar/interval/test_arithmetic.py | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pandas/tests/arithmetic/test_interval.py b/pandas/tests/arithmetic/test_interval.py index f9e1a515277d5..3f85ac8c190db 100644 --- a/pandas/tests/arithmetic/test_interval.py +++ b/pandas/tests/arithmetic/test_interval.py @@ -129,6 +129,10 @@ def test_compare_scalar_interval_mixed_closed(self, op, closed, other_closed): def test_compare_scalar_na(self, op, array, nulls_fixture): result = op(array, nulls_fixture) expected = self.elementwise_comparison(op, array, nulls_fixture) + + if nulls_fixture is pd.NA and array.dtype != pd.IntervalDtype("int"): + pytest.xfail("broken for non-integer IntervalArray; see GH 31882") + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( @@ -207,6 +211,10 @@ def test_compare_list_like_nan(self, op, array, nulls_fixture): other = [nulls_fixture] * 4 result = op(array, other) expected = self.elementwise_comparison(op, array, other) + + if nulls_fixture is pd.NA: + pytest.xfail("broken for non-integer IntervalArray; see GH 31882") + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize( diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py index 38d54da192e40..a3e5912783aae 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -25,12 +25,14 @@ def test_timestamp_interval_add_subtract_timedelta(method, delta): assert result == expected -def test_timedelta_add_timestamp_interval(): +@pytest.mark.parametrize( + "delta", [Timedelta(days=7), timedelta(7), timedelta64(7, "D")] +) +def test_timedelta_add_timestamp_interval(delta): # https://github.com/pandas-dev/pandas/issues/32023 interval = Interval( Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") ) - delta = Timedelta(days=7) result = delta + interval From 784706fe9f547f7d7424714df891d9b514938e00 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Thu, 2 Apr 2020 14:43:28 -0500 Subject: [PATCH 09/16] Check some other types --- pandas/_libs/interval.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 52f81f2e4b21c..a2ec3a2987020 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -1,5 +1,6 @@ import numbers from operator import le, lt +from datetime import timedelta from cpython.object cimport ( Py_EQ, @@ -398,14 +399,14 @@ cdef class Interval(IntervalMixin): return f'{start_symbol}{left}, {right}{end_symbol}' def __add__(self, y): - if isinstance(y, (numbers.Number, Timedelta)): + if isinstance(y, (numbers.Number, Timedelta, timedelta)): 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, Timedelta, timedelta): return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented def __sub__(self, y): - if isinstance(y, (numbers.Number, Timedelta)): + if isinstance(y, (numbers.Number, Timedelta, timedelta)): return Interval(self.left - y, self.right - y, closed=self.closed) return NotImplemented From 51c3c665a4261f82464c7f87fb296f7a0769ad46 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 10 Apr 2020 11:50:07 -0500 Subject: [PATCH 10/16] Maybe fix something --- pandas/_libs/interval.pyx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index a2ec3a2987020..8c755a75df1ac 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -1,6 +1,8 @@ import numbers from operator import le, lt -from datetime import timedelta + +from cpython.datetime cimport PyDelta_Check, PyDateTime_IMPORT +PyDateTime_IMPORT from cpython.object cimport ( Py_EQ, @@ -12,7 +14,6 @@ from cpython.object cimport ( PyObject_RichCompare, ) - import cython from cython import Py_ssize_t @@ -399,14 +400,14 @@ cdef class Interval(IntervalMixin): return f'{start_symbol}{left}, {right}{end_symbol}' def __add__(self, y): - if isinstance(y, (numbers.Number, Timedelta, timedelta)): + if isinstance(y, numbers.Number) or PyDelta_Check(y): return Interval(self.left + y, self.right + y, closed=self.closed) - elif isinstance(y, Interval) and isinstance(self, numbers.Number, Timedelta, timedelta): + elif isinstance(y, Interval) and (isinstance(self, numbers.Number) or PyDelta_Check(self)): return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented def __sub__(self, y): - if isinstance(y, (numbers.Number, Timedelta, timedelta)): + if isinstance(y, numbers.Number) or PyDelta_Check(y): return Interval(self.left - y, self.right - y, closed=self.closed) return NotImplemented From 9ff29a94f212fb00eed97bb327fa3e80af8dabf4 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 11 Apr 2020 09:20:48 -0500 Subject: [PATCH 11/16] Remove np.timdelta64 test case for now --- pandas/tests/scalar/interval/test_arithmetic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py index a3e5912783aae..6fc0aacafaee2 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -26,7 +26,7 @@ def test_timestamp_interval_add_subtract_timedelta(method, delta): @pytest.mark.parametrize( - "delta", [Timedelta(days=7), timedelta(7), timedelta64(7, "D")] + "delta", [Timedelta(days=7), timedelta(7)] ) def test_timedelta_add_timestamp_interval(delta): # https://github.com/pandas-dev/pandas/issues/32023 From ab97990192a7045771b14519bbfbfd87135f6db4 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 11 Apr 2020 09:54:14 -0500 Subject: [PATCH 12/16] Lint --- pandas/_libs/interval.pyx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index b760c87705b3b..ce091197fe509 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -402,7 +402,10 @@ cdef class Interval(IntervalMixin): def __add__(self, y): if isinstance(y, numbers.Number) or PyDelta_Check(y): return Interval(self.left + y, self.right + y, closed=self.closed) - elif isinstance(y, Interval) and (isinstance(self, numbers.Number) or PyDelta_Check(self)): + elif ( + isinstance(y, Interval) + and (isinstance(self, numbers.Number) or PyDelta_Check(self)) + ): return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented From 4955456487d0ba52c8d72ff1b85e99b812af4d91 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 11 Apr 2020 10:15:23 -0500 Subject: [PATCH 13/16] Black --- pandas/tests/scalar/interval/test_arithmetic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py index 6fc0aacafaee2..24d171ccf0952 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -25,9 +25,7 @@ def test_timestamp_interval_add_subtract_timedelta(method, delta): assert result == expected -@pytest.mark.parametrize( - "delta", [Timedelta(days=7), timedelta(7)] -) +@pytest.mark.parametrize("delta", [Timedelta(days=7), timedelta(7)]) def test_timedelta_add_timestamp_interval(delta): # https://github.com/pandas-dev/pandas/issues/32023 interval = Interval( From 985f94eb2c6579f2db27c689c81165c76d100d56 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 11 Apr 2020 13:38:50 -0500 Subject: [PATCH 14/16] timedelta64 -> np.timedelta64 --- pandas/tests/scalar/interval/test_arithmetic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py index 24d171ccf0952..1bd0ed3231970 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -1,6 +1,6 @@ from datetime import timedelta -from numpy import timedelta64 +import numpy as np import pytest from pandas import Interval, Timedelta, Timestamp @@ -8,7 +8,7 @@ @pytest.mark.parametrize("method", ["__add__", "__sub__"]) @pytest.mark.parametrize( - "delta", [Timedelta(days=7), timedelta(7), timedelta64(7, "D")] + "delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")] ) def test_timestamp_interval_add_subtract_timedelta(method, delta): # https://github.com/pandas-dev/pandas/issues/32023 From 0c3a0ed59880bdc17bf20c018eb9f1d0e84bb6d4 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 11 Apr 2020 17:50:48 -0500 Subject: [PATCH 15/16] __array_priority__ --- pandas/_libs/interval.pyx | 1 + pandas/tests/scalar/interval/test_arithmetic.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index ce091197fe509..ac479dc01c08f 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -296,6 +296,7 @@ cdef class Interval(IntervalMixin): True """ _typ = "interval" + __array_priority__ = 1000 cdef readonly object left """ diff --git a/pandas/tests/scalar/interval/test_arithmetic.py b/pandas/tests/scalar/interval/test_arithmetic.py index 1bd0ed3231970..45b3646a2a656 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -25,7 +25,9 @@ def test_timestamp_interval_add_subtract_timedelta(method, delta): assert result == expected -@pytest.mark.parametrize("delta", [Timedelta(days=7), timedelta(7)]) +@pytest.mark.parametrize( + "delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")] +) def test_timedelta_add_timestamp_interval(delta): # https://github.com/pandas-dev/pandas/issues/32023 interval = Interval( From 3d2f91312762547f03326cb751b4d00d81c151f2 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Wed, 15 Apr 2020 18:17:15 -0500 Subject: [PATCH 16/16] Update --- pandas/_libs/interval.pyx | 24 ++++++++-- .../tests/scalar/interval/test_arithmetic.py | 47 ++++++++----------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index ac479dc01c08f..e52474e233510 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -36,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 @@ -401,17 +405,29 @@ cdef class Interval(IntervalMixin): return f'{start_symbol}{left}, {right}{end_symbol}' def __add__(self, y): - if isinstance(y, numbers.Number) or PyDelta_Check(y): + 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) or PyDelta_Check(self)) + 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) or PyDelta_Check(y): + 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 index 45b3646a2a656..5252f1a4d5a24 100644 --- a/pandas/tests/scalar/interval/test_arithmetic.py +++ b/pandas/tests/scalar/interval/test_arithmetic.py @@ -7,17 +7,19 @@ @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_timestamp_interval_add_subtract_timedelta(method, delta): +def test_time_interval_add_subtract_timedelta(interval, delta, method): # https://github.com/pandas-dev/pandas/issues/32023 - interval = Interval( - Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") - ) - result = getattr(interval, method)(delta) - left = getattr(interval.left, method)(delta) right = getattr(interval.right, method)(delta) expected = Interval(left, right) @@ -25,30 +27,21 @@ def test_timestamp_interval_add_subtract_timedelta(method, delta): 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_timedelta_add_timestamp_interval(delta): +def test_numeric_interval_add_timedelta_raises(interval, delta): # https://github.com/pandas-dev/pandas/issues/32023 - interval = Interval( - Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00") + msg = "|".join( + [ + "unsupported operand", + "cannot use operands", + "Only numeric, Timestamp and Timedelta endpoints are allowed", + ] ) + with pytest.raises((TypeError, ValueError), match=msg): + interval + delta - result = delta + interval - - left = interval.left + delta - right = interval.right + delta - expected = Interval(left, right) - - assert result == expected - - -@pytest.mark.parametrize("method", ["__add__", "__sub__"]) -def test_numeric_interval_add_subtract_timedelta_raises(method): - # https://github.com/pandas-dev/pandas/issues/32023 - interval = Interval(1, 2) - delta = Timedelta(days=7) - - msg = "unsupported operand" - with pytest.raises(TypeError, match=msg): - getattr(interval, method)(delta) + with pytest.raises((TypeError, ValueError), match=msg): + delta + interval