Skip to content

Commit 15ad542

Browse files
jschendeljreback
authored andcommitted
BUG: Fix tz-aware DatetimeIndex +/- TimedeltaIndex/timedelta64 array (#18653)
1 parent d916351 commit 15ad542

File tree

4 files changed

+107
-10
lines changed

4 files changed

+107
-10
lines changed

doc/source/whatsnew/v0.22.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Indexing
262262
- Bug in :class:`IntervalIndex` where empty and purely NA data was constructed inconsistently depending on the construction method (:issue:`18421`)
263263
- Bug in :func:`IntervalIndex.symmetric_difference` where the symmetric difference with a non-``IntervalIndex`` did not raise (:issue:`18475`)
264264
- Bug in indexing a datetimelike ``Index`` that raised ``ValueError`` instead of ``IndexError`` (:issue:`18386`).
265-
265+
- Bug in tz-aware :class:`DatetimeIndex` where addition/subtraction with a :class:`TimedeltaIndex` or array with ``dtype='timedelta64[ns]'`` was incorrect (:issue:`17558`)
266266

267267
I/O
268268
^^^

pandas/core/indexes/datetimelike.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
is_integer, is_float,
1515
is_bool_dtype, _ensure_int64,
1616
is_scalar, is_dtype_equal,
17-
is_list_like)
17+
is_list_like, is_timedelta64_dtype)
1818
from pandas.core.dtypes.generic import (
1919
ABCIndex, ABCSeries,
2020
ABCPeriodIndex, ABCIndexClass)
@@ -651,14 +651,14 @@ def __add__(self, other):
651651
from pandas.core.index import Index
652652
from pandas.core.indexes.timedeltas import TimedeltaIndex
653653
from pandas.tseries.offsets import DateOffset
654-
if isinstance(other, TimedeltaIndex):
654+
if is_timedelta64_dtype(other):
655655
return self._add_delta(other)
656656
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
657657
if hasattr(other, '_add_delta'):
658658
return other._add_delta(self)
659659
raise TypeError("cannot add TimedeltaIndex and {typ}"
660660
.format(typ=type(other)))
661-
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
661+
elif isinstance(other, (DateOffset, timedelta)):
662662
return self._add_delta(other)
663663
elif is_integer(other):
664664
return self.shift(other)
@@ -674,7 +674,7 @@ def __sub__(self, other):
674674
from pandas.core.indexes.datetimes import DatetimeIndex
675675
from pandas.core.indexes.timedeltas import TimedeltaIndex
676676
from pandas.tseries.offsets import DateOffset
677-
if isinstance(other, TimedeltaIndex):
677+
if is_timedelta64_dtype(other):
678678
return self._add_delta(-other)
679679
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
680680
if not isinstance(other, TimedeltaIndex):
@@ -687,7 +687,7 @@ def __sub__(self, other):
687687
raise TypeError("cannot subtract {typ1} and {typ2}"
688688
.format(typ1=type(self).__name__,
689689
typ2=type(other).__name__))
690-
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
690+
elif isinstance(other, (DateOffset, timedelta)):
691691
return self._add_delta(-other)
692692
elif is_integer(other):
693693
return self.shift(-other)
@@ -736,7 +736,7 @@ def _add_delta_tdi(self, other):
736736
if self.hasnans or other.hasnans:
737737
mask = (self._isnan) | (other._isnan)
738738
new_values[mask] = iNaT
739-
return new_values.view(self.dtype)
739+
return new_values.view('i8')
740740

741741
def isin(self, values):
742742
"""

pandas/core/indexes/datetimes.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
_NS_DTYPE, _INT64_DTYPE,
1414
is_object_dtype, is_datetime64_dtype,
1515
is_datetimetz, is_dtype_equal,
16+
is_timedelta64_dtype,
1617
is_integer, is_float,
1718
is_integer_dtype,
1819
is_datetime64_ns_dtype,
@@ -858,10 +859,13 @@ def _add_delta(self, delta):
858859

859860
if isinstance(delta, (Tick, timedelta, np.timedelta64)):
860861
new_values = self._add_delta_td(delta)
861-
elif isinstance(delta, TimedeltaIndex):
862+
elif is_timedelta64_dtype(delta):
863+
if not isinstance(delta, TimedeltaIndex):
864+
delta = TimedeltaIndex(delta)
865+
else:
866+
# update name when delta is Index
867+
name = com._maybe_match_name(self, delta)
862868
new_values = self._add_delta_tdi(delta)
863-
# update name when delta is Index
864-
name = com._maybe_match_name(self, delta)
865869
elif isinstance(delta, DateOffset):
866870
new_values = self._add_offset(delta).asi8
867871
else:

pandas/tests/indexes/datetimes/test_arithmetic.py

+93
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,99 @@ def test_dti_isub_timedeltalike(self, tz, delta):
121121
rng -= delta
122122
tm.assert_index_equal(rng, expected)
123123

124+
# -------------------------------------------------------------
125+
# Binary operations DatetimeIndex and TimedeltaIndex/array
126+
def test_dti_add_tdi(self, tz):
127+
# GH 17558
128+
dti = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
129+
tdi = pd.timedelta_range('0 days', periods=10)
130+
expected = pd.date_range('2017-01-01', periods=10, tz=tz)
131+
132+
# add with TimdeltaIndex
133+
result = dti + tdi
134+
tm.assert_index_equal(result, expected)
135+
136+
result = tdi + dti
137+
tm.assert_index_equal(result, expected)
138+
139+
# add with timedelta64 array
140+
result = dti + tdi.values
141+
tm.assert_index_equal(result, expected)
142+
143+
result = tdi.values + dti
144+
tm.assert_index_equal(result, expected)
145+
146+
def test_dti_iadd_tdi(self, tz):
147+
# GH 17558
148+
dti = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
149+
tdi = pd.timedelta_range('0 days', periods=10)
150+
expected = pd.date_range('2017-01-01', periods=10, tz=tz)
151+
152+
# iadd with TimdeltaIndex
153+
result = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
154+
result += tdi
155+
tm.assert_index_equal(result, expected)
156+
157+
result = pd.timedelta_range('0 days', periods=10)
158+
result += dti
159+
tm.assert_index_equal(result, expected)
160+
161+
# iadd with timedelta64 array
162+
result = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
163+
result += tdi.values
164+
tm.assert_index_equal(result, expected)
165+
166+
result = pd.timedelta_range('0 days', periods=10)
167+
result += dti
168+
tm.assert_index_equal(result, expected)
169+
170+
def test_dti_sub_tdi(self, tz):
171+
# GH 17558
172+
dti = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
173+
tdi = pd.timedelta_range('0 days', periods=10)
174+
expected = pd.date_range('2017-01-01', periods=10, tz=tz, freq='-1D')
175+
176+
# sub with TimedeltaIndex
177+
result = dti - tdi
178+
tm.assert_index_equal(result, expected)
179+
180+
msg = 'cannot subtract TimedeltaIndex and DatetimeIndex'
181+
with tm.assert_raises_regex(TypeError, msg):
182+
tdi - dti
183+
184+
# sub with timedelta64 array
185+
result = dti - tdi.values
186+
tm.assert_index_equal(result, expected)
187+
188+
msg = 'cannot perform __neg__ with this index type:'
189+
with tm.assert_raises_regex(TypeError, msg):
190+
tdi.values - dti
191+
192+
def test_dti_isub_tdi(self, tz):
193+
# GH 17558
194+
dti = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
195+
tdi = pd.timedelta_range('0 days', periods=10)
196+
expected = pd.date_range('2017-01-01', periods=10, tz=tz, freq='-1D')
197+
198+
# isub with TimedeltaIndex
199+
result = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
200+
result -= tdi
201+
tm.assert_index_equal(result, expected)
202+
203+
msg = 'cannot subtract TimedeltaIndex and DatetimeIndex'
204+
with tm.assert_raises_regex(TypeError, msg):
205+
tdi -= dti
206+
207+
# isub with timedelta64 array
208+
result = DatetimeIndex([Timestamp('2017-01-01', tz=tz)] * 10)
209+
result -= tdi.values
210+
tm.assert_index_equal(result, expected)
211+
212+
msg = '|'.join(['cannot perform __neg__ with this index type:',
213+
'ufunc subtract cannot use operands with types'])
214+
with tm.assert_raises_regex(TypeError, msg):
215+
tdi.values -= dti
216+
124217
# -------------------------------------------------------------
125218
# Binary Operations DatetimeIndex and datetime-like
126219
# TODO: A couple other tests belong in this section. Move them in

0 commit comments

Comments
 (0)