Skip to content

Commit c6e3e15

Browse files
authored
BUG: Allow addition of Timedelta to Timestamp interval (#32107)
1 parent f5b6e99 commit c6e3e15

File tree

3 files changed

+75
-5
lines changed

3 files changed

+75
-5
lines changed

doc/source/whatsnew/v1.1.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ Indexing
559559
- Bug in :class:`Index` constructor where an unhelpful error message was raised for ``numpy`` scalars (:issue:`33017`)
560560
- 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`)
561561
- Bug in :meth:`DataFrame.iloc.__setitem__` creating a new array instead of overwriting ``Categorical`` values in-place (:issue:`32831`)
562+
- Bug in :class:`Interval` where a :class:`Timedelta` could not be added or subtracted from a :class:`Timestamp` interval (:issue:`32023`)
562563
- Bug in :meth:`DataFrame.copy` _item_cache not invalidated after copy causes post-copy value updates to not be reflected (:issue:`31784`)
563564
- 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`)
564565
- 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`)

pandas/_libs/interval.pyx

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import numbers
22
from operator import le, lt
33

4+
from cpython.datetime cimport PyDelta_Check, PyDateTime_IMPORT
5+
PyDateTime_IMPORT
6+
47
from cpython.object cimport (
58
Py_EQ,
69
Py_GE,
@@ -11,7 +14,6 @@ from cpython.object cimport (
1114
PyObject_RichCompare,
1215
)
1316

14-
1517
import cython
1618
from cython import Py_ssize_t
1719

@@ -34,7 +36,11 @@ cnp.import_array()
3436
cimport pandas._libs.util as util
3537

3638
from pandas._libs.hashtable cimport Int64Vector
37-
from pandas._libs.tslibs.util cimport is_integer_object, is_float_object
39+
from pandas._libs.tslibs.util cimport (
40+
is_integer_object,
41+
is_float_object,
42+
is_timedelta64_object,
43+
)
3844

3945
from pandas._libs.tslibs import Timestamp
4046
from pandas._libs.tslibs.timedeltas import Timedelta
@@ -294,6 +300,7 @@ cdef class Interval(IntervalMixin):
294300
True
295301
"""
296302
_typ = "interval"
303+
__array_priority__ = 1000
297304

298305
cdef readonly object left
299306
"""
@@ -398,14 +405,29 @@ cdef class Interval(IntervalMixin):
398405
return f'{start_symbol}{left}, {right}{end_symbol}'
399406

400407
def __add__(self, y):
401-
if isinstance(y, numbers.Number):
408+
if (
409+
isinstance(y, numbers.Number)
410+
or PyDelta_Check(y)
411+
or is_timedelta64_object(y)
412+
):
402413
return Interval(self.left + y, self.right + y, closed=self.closed)
403-
elif isinstance(y, Interval) and isinstance(self, numbers.Number):
414+
elif (
415+
isinstance(y, Interval)
416+
and (
417+
isinstance(self, numbers.Number)
418+
or PyDelta_Check(self)
419+
or is_timedelta64_object(self)
420+
)
421+
):
404422
return Interval(y.left + self, y.right + self, closed=y.closed)
405423
return NotImplemented
406424

407425
def __sub__(self, y):
408-
if isinstance(y, numbers.Number):
426+
if (
427+
isinstance(y, numbers.Number)
428+
or PyDelta_Check(y)
429+
or is_timedelta64_object(y)
430+
):
409431
return Interval(self.left - y, self.right - y, closed=self.closed)
410432
return NotImplemented
411433

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from datetime import timedelta
2+
3+
import numpy as np
4+
import pytest
5+
6+
from pandas import Interval, Timedelta, Timestamp
7+
8+
9+
@pytest.mark.parametrize("method", ["__add__", "__sub__"])
10+
@pytest.mark.parametrize(
11+
"interval",
12+
[
13+
Interval(Timestamp("2017-01-01 00:00:00"), Timestamp("2018-01-01 00:00:00")),
14+
Interval(Timedelta(days=7), Timedelta(days=14)),
15+
],
16+
)
17+
@pytest.mark.parametrize(
18+
"delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")]
19+
)
20+
def test_time_interval_add_subtract_timedelta(interval, delta, method):
21+
# https://github.com/pandas-dev/pandas/issues/32023
22+
result = getattr(interval, method)(delta)
23+
left = getattr(interval.left, method)(delta)
24+
right = getattr(interval.right, method)(delta)
25+
expected = Interval(left, right)
26+
27+
assert result == expected
28+
29+
30+
@pytest.mark.parametrize("interval", [Interval(1, 2), Interval(1.0, 2.0)])
31+
@pytest.mark.parametrize(
32+
"delta", [Timedelta(days=7), timedelta(7), np.timedelta64(7, "D")]
33+
)
34+
def test_numeric_interval_add_timedelta_raises(interval, delta):
35+
# https://github.com/pandas-dev/pandas/issues/32023
36+
msg = "|".join(
37+
[
38+
"unsupported operand",
39+
"cannot use operands",
40+
"Only numeric, Timestamp and Timedelta endpoints are allowed",
41+
]
42+
)
43+
with pytest.raises((TypeError, ValueError), match=msg):
44+
interval + delta
45+
46+
with pytest.raises((TypeError, ValueError), match=msg):
47+
delta + interval

0 commit comments

Comments
 (0)