Skip to content

Commit e975455

Browse files
jbrockmendelharisbal
authored and
harisbal
committed
Fix name setting in DTI/TDI __add__ and __sub__ (pandas-dev#19744)
1 parent 8875ecb commit e975455

File tree

11 files changed

+240
-118
lines changed

11 files changed

+240
-118
lines changed

doc/source/whatsnew/v0.23.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,8 @@ Datetimelike
731731
- Bug in :class:`Timestamp` and :func:`to_datetime` where a string representing a barely out-of-bounds timestamp would be incorrectly rounded down instead of raising ``OutOfBoundsDatetime`` (:issue:`19382`)
732732
- Bug in :func:`Timestamp.floor` :func:`DatetimeIndex.floor` where time stamps far in the future and past were not rounded correctly (:issue:`19206`)
733733
- 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`)
734+
- Bug in :class:`DatetimeIndex` and :class:`TimedeltaIndex` addition and subtraction where name of the returned object was not always set consistently. (:issue:`19744`)
735+
-
734736

735737
Timedelta
736738
^^^^^^^^^

pandas/core/common.py

-15
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,6 @@ def _consensus_name_attr(objs):
121121
return name
122122

123123

124-
def _maybe_match_name(a, b):
125-
a_has = hasattr(a, 'name')
126-
b_has = hasattr(b, 'name')
127-
if a_has and b_has:
128-
if a.name == b.name:
129-
return a.name
130-
else:
131-
return None
132-
elif a_has:
133-
return a.name
134-
elif b_has:
135-
return b.name
136-
return None
137-
138-
139124
def _get_info_slice(obj, indexer):
140125
"""Slice the info axis of `obj` with `indexer`."""
141126
if not hasattr(obj, '_info_axis_number'):

pandas/core/indexes/datetimelike.py

+33-18
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from pandas.core.dtypes.generic import (
3030
ABCIndex, ABCSeries, ABCPeriodIndex, ABCIndexClass)
3131
from pandas.core.dtypes.missing import isna
32-
from pandas.core import common as com, algorithms
32+
from pandas.core import common as com, algorithms, ops
3333
from pandas.core.algorithms import checked_add_with_arr
3434
from pandas.errors import NullFrequencyError
3535
import pandas.io.formats.printing as printing
@@ -661,29 +661,37 @@ def __add__(self, other):
661661
if isinstance(other, ABCSeries):
662662
return NotImplemented
663663
elif is_timedelta64_dtype(other):
664-
return self._add_delta(other)
664+
result = self._add_delta(other)
665665
elif isinstance(other, (DateOffset, timedelta)):
666-
return self._add_delta(other)
666+
result = self._add_delta(other)
667667
elif is_offsetlike(other):
668668
# Array/Index of DateOffset objects
669-
return self._add_offset_array(other)
669+
result = self._add_offset_array(other)
670670
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
671671
if hasattr(other, '_add_delta'):
672-
return other._add_delta(self)
673-
raise TypeError("cannot add TimedeltaIndex and {typ}"
674-
.format(typ=type(other)))
672+
result = other._add_delta(self)
673+
else:
674+
raise TypeError("cannot add TimedeltaIndex and {typ}"
675+
.format(typ=type(other)))
675676
elif is_integer(other):
676-
return self.shift(other)
677+
# This check must come after the check for timedelta64_dtype
678+
# or else it will incorrectly catch np.timedelta64 objects
679+
result = self.shift(other)
677680
elif isinstance(other, (datetime, np.datetime64)):
678-
return self._add_datelike(other)
681+
result = self._add_datelike(other)
679682
elif isinstance(other, Index):
680-
return self._add_datelike(other)
683+
result = self._add_datelike(other)
681684
elif is_integer_dtype(other) and self.freq is None:
682685
# GH#19123
683686
raise NullFrequencyError("Cannot shift with no freq")
684687
else: # pragma: no cover
685688
return NotImplemented
686689

690+
if result is not NotImplemented:
691+
res_name = ops.get_op_result_name(self, other)
692+
result.name = res_name
693+
return result
694+
687695
cls.__add__ = __add__
688696
cls.__radd__ = __add__
689697

@@ -697,25 +705,27 @@ def __sub__(self, other):
697705
if isinstance(other, ABCSeries):
698706
return NotImplemented
699707
elif is_timedelta64_dtype(other):
700-
return self._add_delta(-other)
708+
result = self._add_delta(-other)
701709
elif isinstance(other, (DateOffset, timedelta)):
702-
return self._add_delta(-other)
710+
result = self._add_delta(-other)
703711
elif is_offsetlike(other):
704712
# Array/Index of DateOffset objects
705-
return self._sub_offset_array(other)
713+
result = self._sub_offset_array(other)
706714
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
707715
if not isinstance(other, TimedeltaIndex):
708716
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
709717
.format(typ=type(other).__name__))
710-
return self._add_delta(-other)
718+
result = self._add_delta(-other)
711719
elif isinstance(other, DatetimeIndex):
712-
return self._sub_datelike(other)
720+
result = self._sub_datelike(other)
713721
elif is_integer(other):
714-
return self.shift(-other)
722+
# This check must come after the check for timedelta64_dtype
723+
# or else it will incorrectly catch np.timedelta64 objects
724+
result = self.shift(-other)
715725
elif isinstance(other, (datetime, np.datetime64)):
716-
return self._sub_datelike(other)
726+
result = self._sub_datelike(other)
717727
elif isinstance(other, Period):
718-
return self._sub_period(other)
728+
result = self._sub_period(other)
719729
elif isinstance(other, Index):
720730
raise TypeError("cannot subtract {typ1} and {typ2}"
721731
.format(typ1=type(self).__name__,
@@ -726,6 +736,11 @@ def __sub__(self, other):
726736
else: # pragma: no cover
727737
return NotImplemented
728738

739+
if result is not NotImplemented:
740+
res_name = ops.get_op_result_name(self, other)
741+
result.name = res_name
742+
return result
743+
729744
cls.__sub__ = __sub__
730745

731746
def __rsub__(self, other):

pandas/core/indexes/datetimes.py

+22-14
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ def _sub_datelike(self, other):
886886
else:
887887
raise TypeError("cannot subtract DatetimeIndex and {typ}"
888888
.format(typ=type(other).__name__))
889-
return TimedeltaIndex(result, name=self.name, copy=False)
889+
return TimedeltaIndex(result)
890890

891891
def _sub_datelike_dti(self, other):
892892
"""subtraction of two DatetimeIndexes"""
@@ -910,28 +910,39 @@ def _maybe_update_attributes(self, attrs):
910910
return attrs
911911

912912
def _add_delta(self, delta):
913-
if isinstance(delta, ABCSeries):
914-
return NotImplemented
913+
"""
914+
Add a timedelta-like, DateOffset, or TimedeltaIndex-like object
915+
to self.
916+
917+
Parameters
918+
----------
919+
delta : {timedelta, np.timedelta64, DateOffset,
920+
TimedelaIndex, ndarray[timedelta64]}
915921
922+
Returns
923+
-------
924+
result : DatetimeIndex
925+
926+
Notes
927+
-----
928+
The result's name is set outside of _add_delta by the calling
929+
method (__add__ or __sub__)
930+
"""
916931
from pandas import TimedeltaIndex
917-
name = self.name
918932

919933
if isinstance(delta, (Tick, timedelta, np.timedelta64)):
920934
new_values = self._add_delta_td(delta)
921935
elif is_timedelta64_dtype(delta):
922936
if not isinstance(delta, TimedeltaIndex):
923937
delta = TimedeltaIndex(delta)
924-
else:
925-
# update name when delta is Index
926-
name = com._maybe_match_name(self, delta)
927938
new_values = self._add_delta_tdi(delta)
928939
elif isinstance(delta, DateOffset):
929940
new_values = self._add_offset(delta).asi8
930941
else:
931942
new_values = self.astype('O') + delta
932943

933944
tz = 'UTC' if self.tz is not None else None
934-
result = DatetimeIndex(new_values, tz=tz, name=name, freq='infer')
945+
result = DatetimeIndex(new_values, tz=tz, freq='infer')
935946
if self.tz is not None and self.tz is not utc:
936947
result = result.tz_convert(self.tz)
937948
return result
@@ -954,22 +965,19 @@ def _add_offset(self, offset):
954965

955966
def _add_offset_array(self, other):
956967
# Array/Index of DateOffset objects
957-
if isinstance(other, ABCSeries):
958-
return NotImplemented
959-
elif len(other) == 1:
968+
if len(other) == 1:
960969
return self + other[0]
961970
else:
962971
warnings.warn("Adding/subtracting array of DateOffsets to "
963972
"{} not vectorized".format(type(self)),
964973
PerformanceWarning)
965974
return self.astype('O') + np.array(other)
975+
# TODO: pass freq='infer' like we do in _sub_offset_array?
966976
# TODO: This works for __add__ but loses dtype in __sub__
967977

968978
def _sub_offset_array(self, other):
969979
# Array/Index of DateOffset objects
970-
if isinstance(other, ABCSeries):
971-
return NotImplemented
972-
elif len(other) == 1:
980+
if len(other) == 1:
973981
return self - other[0]
974982
else:
975983
warnings.warn("Adding/subtracting array of DateOffsets to "

pandas/core/indexes/period.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ def _sub_datelike(self, other):
729729
if other is tslib.NaT:
730730
new_data = np.empty(len(self), dtype=np.int64)
731731
new_data.fill(tslib.iNaT)
732-
return TimedeltaIndex(new_data, name=self.name)
732+
return TimedeltaIndex(new_data)
733733
return NotImplemented
734734

735735
def _sub_period(self, other):
@@ -744,7 +744,7 @@ def _sub_period(self, other):
744744
new_data = new_data.astype(np.float64)
745745
new_data[self._isnan] = np.nan
746746
# result must be Int64Index or Float64Index
747-
return Index(new_data, name=self.name)
747+
return Index(new_data)
748748

749749
def shift(self, n):
750750
"""

pandas/core/indexes/timedeltas.py

+22-12
Original file line numberDiff line numberDiff line change
@@ -356,19 +356,32 @@ def _maybe_update_attributes(self, attrs):
356356
return attrs
357357

358358
def _add_delta(self, delta):
359+
"""
360+
Add a timedelta-like, Tick, or TimedeltaIndex-like object
361+
to self.
362+
363+
Parameters
364+
----------
365+
delta : {timedelta, np.timedelta64, Tick, TimedeltaIndex}
366+
367+
Returns
368+
-------
369+
result : TimedeltaIndex
370+
371+
Notes
372+
-----
373+
The result's name is set outside of _add_delta by the calling
374+
method (__add__ or __sub__)
375+
"""
359376
if isinstance(delta, (Tick, timedelta, np.timedelta64)):
360377
new_values = self._add_delta_td(delta)
361-
name = self.name
362378
elif isinstance(delta, TimedeltaIndex):
363379
new_values = self._add_delta_tdi(delta)
364-
# update name when delta is index
365-
name = com._maybe_match_name(self, delta)
366380
else:
367381
raise TypeError("cannot add the type {0} to a TimedeltaIndex"
368382
.format(type(delta)))
369383

370-
result = TimedeltaIndex(new_values, freq='infer', name=name)
371-
return result
384+
return TimedeltaIndex(new_values, freq='infer')
372385

373386
def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):
374387
if isinstance(other, ABCSeries):
@@ -409,7 +422,7 @@ def _add_datelike(self, other):
409422
result = checked_add_with_arr(i8, other.value,
410423
arr_mask=self._isnan)
411424
result = self._maybe_mask_results(result, fill_value=iNaT)
412-
return DatetimeIndex(result, name=self.name, copy=False)
425+
return DatetimeIndex(result)
413426

414427
def _sub_datelike(self, other):
415428
# GH#19124 Timedelta - datetime is not in general well-defined.
@@ -426,16 +439,15 @@ def _add_offset_array(self, other):
426439
# TimedeltaIndex can only operate with a subset of DateOffset
427440
# subclasses. Incompatible classes will raise AttributeError,
428441
# which we re-raise as TypeError
429-
if isinstance(other, ABCSeries):
430-
return NotImplemented
431-
elif len(other) == 1:
442+
if len(other) == 1:
432443
return self + other[0]
433444
else:
434445
from pandas.errors import PerformanceWarning
435446
warnings.warn("Adding/subtracting array of DateOffsets to "
436447
"{} not vectorized".format(type(self)),
437448
PerformanceWarning)
438449
return self.astype('O') + np.array(other)
450+
# TODO: pass freq='infer' like we do in _sub_offset_array?
439451
# TODO: This works for __add__ but loses dtype in __sub__
440452
except AttributeError:
441453
raise TypeError("Cannot add non-tick DateOffset to TimedeltaIndex")
@@ -446,9 +458,7 @@ def _sub_offset_array(self, other):
446458
# TimedeltaIndex can only operate with a subset of DateOffset
447459
# subclasses. Incompatible classes will raise AttributeError,
448460
# which we re-raise as TypeError
449-
if isinstance(other, ABCSeries):
450-
return NotImplemented
451-
elif len(other) == 1:
461+
if len(other) == 1:
452462
return self - other[0]
453463
else:
454464
from pandas.errors import PerformanceWarning

0 commit comments

Comments
 (0)