Skip to content

Commit 5a87765

Browse files
jbrockmendeljreback
authored andcommitted
Fix+test DTI/TDI/PI add/sub with ndarray[datetime64/timedelta64] (#19847)
1 parent c1237f2 commit 5a87765

File tree

6 files changed

+182
-7
lines changed

6 files changed

+182
-7
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@ Datetimelike
756756
- Bug in :func:`Timestamp.floor` :func:`DatetimeIndex.floor` where time stamps far in the future and past were not rounded correctly (:issue:`19206`)
757757
- Bug in :func:`to_datetime` where passing an out-of-bounds datetime with ``errors='coerce'`` and ``utc=True`` would raise ``OutOfBoundsDatetime`` instead of parsing to ``NaT`` (:issue:`19612`)
758758
- Bug in :class:`DatetimeIndex` and :class:`TimedeltaIndex` addition and subtraction where name of the returned object was not always set consistently. (:issue:`19744`)
759+
- Bug in :class:`DatetimeIndex` and :class:`TimedeltaIndex` addition and subtraction where operations with numpy arrays raised ``TypeError`` (:issue:`19847`)
759760
-
760761

761762
Timedelta

pandas/core/indexes/datetimelike.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
is_integer_dtype,
3232
is_object_dtype,
3333
is_string_dtype,
34+
is_datetime64_dtype,
3435
is_period_dtype,
3536
is_timedelta64_dtype)
3637
from pandas.core.dtypes.generic import (
@@ -676,9 +677,7 @@ def _add_datetimelike_methods(cls):
676677
"""
677678

678679
def __add__(self, other):
679-
from pandas.core.index import Index
680-
from pandas.core.indexes.timedeltas import TimedeltaIndex
681-
from pandas.tseries.offsets import DateOffset
680+
from pandas import Index, DatetimeIndex, TimedeltaIndex, DateOffset
682681

683682
other = lib.item_from_zerodim(other)
684683
if isinstance(other, ABCSeries):
@@ -710,6 +709,9 @@ def __add__(self, other):
710709
.format(typ=type(other)))
711710
elif isinstance(other, Index):
712711
result = self._add_datelike(other)
712+
elif is_datetime64_dtype(other):
713+
# ndarray[datetime64]; note DatetimeIndex is caught above
714+
return self + DatetimeIndex(other)
713715
elif is_integer_dtype(other) and self.freq is None:
714716
# GH#19123
715717
raise NullFrequencyError("Cannot shift with no freq")
@@ -729,10 +731,7 @@ def __radd__(self, other):
729731
cls.__radd__ = __radd__
730732

731733
def __sub__(self, other):
732-
from pandas.core.index import Index
733-
from pandas.core.indexes.datetimes import DatetimeIndex
734-
from pandas.core.indexes.timedeltas import TimedeltaIndex
735-
from pandas.tseries.offsets import DateOffset
734+
from pandas import Index, DatetimeIndex, TimedeltaIndex, DateOffset
736735

737736
other = lib.item_from_zerodim(other)
738737
if isinstance(other, ABCSeries):
@@ -764,6 +763,9 @@ def __sub__(self, other):
764763
.format(typ=type(other).__name__))
765764
elif isinstance(other, DatetimeIndex):
766765
result = self._sub_datelike(other)
766+
elif is_datetime64_dtype(other):
767+
# ndarray[datetime64]; note we caught DatetimeIndex earlier
768+
return self - DatetimeIndex(other)
767769
elif isinstance(other, Index):
768770
raise TypeError("cannot subtract {typ1} and {typ2}"
769771
.format(typ1=type(self).__name__,
@@ -782,6 +784,11 @@ def __sub__(self, other):
782784
cls.__sub__ = __sub__
783785

784786
def __rsub__(self, other):
787+
if is_datetime64_dtype(other) and is_timedelta64_dtype(self):
788+
# ndarray[datetime64] cannot be subtracted from self, so
789+
# we need to wrap in DatetimeIndex and flip the operation
790+
from pandas import DatetimeIndex
791+
return DatetimeIndex(other) - self
785792
return -(self - other)
786793
cls.__rsub__ = __rsub__
787794

pandas/core/indexes/timedeltas.py

+4
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ def _add_delta(self, delta):
377377
new_values = self._add_delta_td(delta)
378378
elif isinstance(delta, TimedeltaIndex):
379379
new_values = self._add_delta_tdi(delta)
380+
elif is_timedelta64_dtype(delta):
381+
# ndarray[timedelta64] --> wrap in TimedeltaIndex
382+
delta = TimedeltaIndex(delta)
383+
new_values = self._add_delta_tdi(delta)
380384
else:
381385
raise TypeError("cannot add the type {0} to a TimedeltaIndex"
382386
.format(type(delta)))

pandas/tests/indexes/datetimes/test_arithmetic.py

+56
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,62 @@ def test_add_datetimelike_and_dti_tz(self, addend):
571571
with tm.assert_raises_regex(TypeError, msg):
572572
addend + dti_tz
573573

574+
# -------------------------------------------------------------
575+
# __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64]
576+
577+
def test_dti_add_dt64_array_raises(self, tz):
578+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
579+
dtarr = dti.values
580+
581+
with pytest.raises(TypeError):
582+
dti + dtarr
583+
with pytest.raises(TypeError):
584+
dtarr + dti
585+
586+
def test_dti_sub_dt64_array_naive(self):
587+
dti = pd.date_range('2016-01-01', periods=3, tz=None)
588+
dtarr = dti.values
589+
590+
expected = dti - dti
591+
result = dti - dtarr
592+
tm.assert_index_equal(result, expected)
593+
result = dtarr - dti
594+
tm.assert_index_equal(result, expected)
595+
596+
def test_dti_sub_dt64_array_aware_raises(self, tz):
597+
if tz is None:
598+
return
599+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
600+
dtarr = dti.values
601+
602+
with pytest.raises(TypeError):
603+
dti - dtarr
604+
with pytest.raises(TypeError):
605+
dtarr - dti
606+
607+
def test_dti_add_td64_array(self, tz):
608+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
609+
tdi = dti - dti.shift(1)
610+
tdarr = tdi.values
611+
612+
expected = dti + tdi
613+
result = dti + tdarr
614+
tm.assert_index_equal(result, expected)
615+
result = tdarr + dti
616+
tm.assert_index_equal(result, expected)
617+
618+
def test_dti_sub_td64_array(self, tz):
619+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
620+
tdi = dti - dti.shift(1)
621+
tdarr = tdi.values
622+
623+
expected = dti - tdi
624+
result = dti - tdarr
625+
tm.assert_index_equal(result, expected)
626+
627+
with pytest.raises(TypeError):
628+
tdarr - dti
629+
574630
# -------------------------------------------------------------
575631

576632
def test_sub_dti_dti(self):

pandas/tests/indexes/period/test_arithmetic.py

+58
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,64 @@ def test_comp_nat(self, dtype):
255255

256256

257257
class TestPeriodIndexArithmetic(object):
258+
259+
# -----------------------------------------------------------------
260+
# __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64]
261+
262+
def test_pi_add_sub_dt64_array_raises(self):
263+
rng = pd.period_range('1/1/2000', freq='D', periods=3)
264+
dti = pd.date_range('2016-01-01', periods=3)
265+
dtarr = dti.values
266+
267+
with pytest.raises(TypeError):
268+
rng + dtarr
269+
with pytest.raises(TypeError):
270+
dtarr + rng
271+
272+
with pytest.raises(TypeError):
273+
rng - dtarr
274+
with pytest.raises(TypeError):
275+
dtarr - rng
276+
277+
def test_pi_add_sub_td64_array_non_tick_raises(self):
278+
rng = pd.period_range('1/1/2000', freq='Q', periods=3)
279+
dti = pd.date_range('2016-01-01', periods=3)
280+
tdi = dti - dti.shift(1)
281+
tdarr = tdi.values
282+
283+
with pytest.raises(period.IncompatibleFrequency):
284+
rng + tdarr
285+
with pytest.raises(period.IncompatibleFrequency):
286+
tdarr + rng
287+
288+
with pytest.raises(period.IncompatibleFrequency):
289+
rng - tdarr
290+
with pytest.raises(period.IncompatibleFrequency):
291+
tdarr - rng
292+
293+
@pytest.mark.xfail(reason='op with TimedeltaIndex raises, with ndarray OK')
294+
def test_pi_add_sub_td64_array_tick(self):
295+
rng = pd.period_range('1/1/2000', freq='Q', periods=3)
296+
dti = pd.date_range('2016-01-01', periods=3)
297+
tdi = dti - dti.shift(1)
298+
tdarr = tdi.values
299+
300+
expected = rng + tdi
301+
result = rng + tdarr
302+
tm.assert_index_equal(result, expected)
303+
result = tdarr + rng
304+
tm.assert_index_equal(result, expected)
305+
306+
expected = rng - tdi
307+
result = rng - tdarr
308+
tm.assert_index_equal(result, expected)
309+
310+
with pytest.raises(TypeError):
311+
tdarr - rng
312+
313+
# -----------------------------------------------------------------
314+
# operations with array/Index of DateOffset objects
315+
258316
@pytest.mark.parametrize('box', [np.array, pd.Index])
259317
def test_pi_add_offset_array(self, box):
260318
# GH#18849

pandas/tests/indexes/timedeltas/test_arithmetic.py

+49
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,55 @@ def test_tdi_radd_timestamp(self):
586586
expected = DatetimeIndex(['2011-01-02', '2011-01-03'])
587587
tm.assert_index_equal(result, expected)
588588

589+
# -------------------------------------------------------------
590+
# __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64]
591+
592+
def test_tdi_sub_dt64_array(self):
593+
dti = pd.date_range('2016-01-01', periods=3)
594+
tdi = dti - dti.shift(1)
595+
dtarr = dti.values
596+
597+
with pytest.raises(TypeError):
598+
tdi - dtarr
599+
600+
# TimedeltaIndex.__rsub__
601+
expected = pd.DatetimeIndex(dtarr) - tdi
602+
result = dtarr - tdi
603+
tm.assert_index_equal(result, expected)
604+
605+
def test_tdi_add_dt64_array(self):
606+
dti = pd.date_range('2016-01-01', periods=3)
607+
tdi = dti - dti.shift(1)
608+
dtarr = dti.values
609+
610+
expected = pd.DatetimeIndex(dtarr) + tdi
611+
result = tdi + dtarr
612+
tm.assert_index_equal(result, expected)
613+
result = dtarr + tdi
614+
tm.assert_index_equal(result, expected)
615+
616+
def test_tdi_add_td64_array(self):
617+
dti = pd.date_range('2016-01-01', periods=3)
618+
tdi = dti - dti.shift(1)
619+
tdarr = tdi.values
620+
621+
expected = 2 * tdi
622+
result = tdi + tdarr
623+
tm.assert_index_equal(result, expected)
624+
result = tdarr + tdi
625+
tm.assert_index_equal(result, expected)
626+
627+
def test_tdi_sub_td64_array(self):
628+
dti = pd.date_range('2016-01-01', periods=3)
629+
tdi = dti - dti.shift(1)
630+
tdarr = tdi.values
631+
632+
expected = 0 * tdi
633+
result = tdi - tdarr
634+
tm.assert_index_equal(result, expected)
635+
result = tdarr - tdi
636+
tm.assert_index_equal(result, expected)
637+
589638
# -------------------------------------------------------------
590639

591640
def test_subtraction_ops(self):

0 commit comments

Comments
 (0)