Skip to content

Commit f105cdf

Browse files
jbrockmendeljreback
authored andcommitted
cleanup order of operations kludges (#19895)
1 parent 15bbb28 commit f105cdf

File tree

6 files changed

+49
-52
lines changed

6 files changed

+49
-52
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ Datetimelike API Changes
586586
- Operations between a :class:`Series` with dtype ``dtype='datetime64[ns]'`` and a :class:`PeriodIndex` will correctly raises ``TypeError`` (:issue:`18850`)
587587
- Subtraction of :class:`Series` with timezone-aware ``dtype='datetime64[ns]'`` with mis-matched timezones will raise ``TypeError`` instead of ``ValueError`` (:issue:`18817`)
588588
- :func:`pandas.merge` provides a more informative error message when trying to merge on timezone-aware and timezone-naive columns (:issue:`15800`)
589+
- For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with ``freq=None``, addition or subtraction of integer-dtyped array or ``Index`` will raise ``NullFrequencyError`` instead of ``TypeError`` (:issue:`19895`)
589590

590591
.. _whatsnew_0230.api.other:
591592

pandas/core/indexes/datetimelike.py

+17-31
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
is_object_dtype,
3333
is_string_dtype,
3434
is_datetime64_dtype,
35+
is_datetime64tz_dtype,
3536
is_period_dtype,
3637
is_timedelta64_dtype)
3738
from pandas.core.dtypes.generic import (
@@ -200,8 +201,9 @@ def _evaluate_compare(self, other, op):
200201
if is_bool_dtype(result):
201202
result[mask] = False
202203
return result
204+
205+
result[mask] = iNaT
203206
try:
204-
result[mask] = iNaT
205207
return Index(result)
206208
except TypeError:
207209
return result
@@ -349,7 +351,7 @@ def _nat_new(self, box=True):
349351
return result
350352

351353
attribs = self._get_attributes_dict()
352-
if not isinstance(self, ABCPeriodIndex):
354+
if not is_period_dtype(self):
353355
attribs['freq'] = None
354356
return self._simple_new(result, **attribs)
355357

@@ -631,9 +633,9 @@ def _convert_scalar_indexer(self, key, kind=None):
631633
._convert_scalar_indexer(key, kind=kind))
632634

633635
def _add_datelike(self, other):
634-
raise TypeError("cannot add {0} and {1}"
635-
.format(type(self).__name__,
636-
type(other).__name__))
636+
raise TypeError("cannot add {cls} and {typ}"
637+
.format(cls=type(self).__name__,
638+
typ=type(other).__name__))
637639

638640
def _sub_datelike(self, other):
639641
raise com.AbstractMethodError(self)
@@ -677,7 +679,7 @@ def _add_datetimelike_methods(cls):
677679
"""
678680

679681
def __add__(self, other):
680-
from pandas import Index, DatetimeIndex, TimedeltaIndex, DateOffset
682+
from pandas import DateOffset
681683

682684
other = lib.item_from_zerodim(other)
683685
if isinstance(other, ABCSeries):
@@ -700,18 +702,9 @@ def __add__(self, other):
700702
elif is_offsetlike(other):
701703
# Array/Index of DateOffset objects
702704
result = self._addsub_offset_array(other, operator.add)
703-
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
704-
if hasattr(other, '_add_delta'):
705-
# i.e. DatetimeIndex, TimedeltaIndex, or PeriodIndex
706-
result = other._add_delta(self)
707-
else:
708-
raise TypeError("cannot add TimedeltaIndex and {typ}"
709-
.format(typ=type(other)))
710-
elif isinstance(other, Index):
711-
result = self._add_datelike(other)
712-
elif is_datetime64_dtype(other):
713-
# ndarray[datetime64]; note DatetimeIndex is caught above
714-
return self + DatetimeIndex(other)
705+
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
706+
# DatetimeIndex, ndarray[datetime64]
707+
return self._add_datelike(other)
715708
elif is_integer_dtype(other) and self.freq is None:
716709
# GH#19123
717710
raise NullFrequencyError("Cannot shift with no freq")
@@ -731,7 +724,7 @@ def __radd__(self, other):
731724
cls.__radd__ = __radd__
732725

733726
def __sub__(self, other):
734-
from pandas import Index, DatetimeIndex, TimedeltaIndex, DateOffset
727+
from pandas import Index, DateOffset
735728

736729
other = lib.item_from_zerodim(other)
737730
if isinstance(other, ABCSeries):
@@ -756,20 +749,13 @@ def __sub__(self, other):
756749
elif is_offsetlike(other):
757750
# Array/Index of DateOffset objects
758751
result = self._addsub_offset_array(other, operator.sub)
759-
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
760-
# We checked above for timedelta64_dtype(other) so this
761-
# must be invalid.
762-
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
763-
.format(typ=type(other).__name__))
764-
elif isinstance(other, DatetimeIndex):
752+
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
753+
# DatetimeIndex, ndarray[datetime64]
765754
result = self._sub_datelike(other)
766-
elif is_datetime64_dtype(other):
767-
# ndarray[datetime64]; note we caught DatetimeIndex earlier
768-
return self - DatetimeIndex(other)
769755
elif isinstance(other, Index):
770-
raise TypeError("cannot subtract {typ1} and {typ2}"
771-
.format(typ1=type(self).__name__,
772-
typ2=type(other).__name__))
756+
raise TypeError("cannot subtract {cls} and {typ}"
757+
.format(cls=type(self).__name__,
758+
typ=type(other).__name__))
773759
elif is_integer_dtype(other) and self.freq is None:
774760
# GH#19123
775761
raise NullFrequencyError("Cannot shift with no freq")

pandas/core/indexes/datetimes.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -864,11 +864,16 @@ def _add_datelike(self, other):
864864
def _sub_datelike(self, other):
865865
# subtract a datetime from myself, yielding a TimedeltaIndex
866866
from pandas import TimedeltaIndex
867-
if isinstance(other, DatetimeIndex):
867+
868+
if isinstance(other, (DatetimeIndex, np.ndarray)):
869+
# if other is an ndarray, we assume it is datetime64-dtype
870+
other = DatetimeIndex(other)
871+
868872
# require tz compat
869873
if not self._has_same_tz(other):
870-
raise TypeError("DatetimeIndex subtraction must have the same "
871-
"timezones or no timezones")
874+
raise TypeError("{cls} subtraction must have the same "
875+
"timezones or no timezones"
876+
.format(cls=type(self).__name__))
872877
result = self._sub_datelike_dti(other)
873878
elif isinstance(other, (datetime, np.datetime64)):
874879
other = Timestamp(other)
@@ -885,8 +890,9 @@ def _sub_datelike(self, other):
885890
result = self._maybe_mask_results(result,
886891
fill_value=libts.iNaT)
887892
else:
888-
raise TypeError("cannot subtract DatetimeIndex and {typ}"
889-
.format(typ=type(other).__name__))
893+
raise TypeError("cannot subtract {cls} and {typ}"
894+
.format(cls=type(self).__name__,
895+
typ=type(other).__name__))
890896
return TimedeltaIndex(result)
891897

892898
def _sub_datelike_dti(self, other):

pandas/core/indexes/timedeltas.py

+17-13
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,28 @@ def _td_index_cmp(opname, cls):
5959
nat_result = True if opname == '__ne__' else False
6060

6161
def wrapper(self, other):
62-
msg = "cannot compare a TimedeltaIndex with type {0}"
62+
msg = "cannot compare a {cls} with type {typ}"
6363
func = getattr(super(TimedeltaIndex, self), opname)
6464
if _is_convertible_to_td(other) or other is NaT:
6565
try:
6666
other = _to_m8(other)
6767
except ValueError:
6868
# failed to parse as timedelta
69-
raise TypeError(msg.format(type(other)))
69+
raise TypeError(msg.format(cls=type(self).__name__,
70+
typ=type(other).__name__))
7071
result = func(other)
7172
if isna(other):
7273
result.fill(nat_result)
73-
else:
74-
if not is_list_like(other):
75-
raise TypeError(msg.format(type(other)))
7674

75+
elif not is_list_like(other):
76+
raise TypeError(msg.format(cls=type(self).__name__,
77+
typ=type(other).__name__))
78+
else:
7779
other = TimedeltaIndex(other).values
7880
result = func(other)
7981
result = com._values_from_object(result)
8082

81-
if isinstance(other, Index):
82-
o_mask = other.values.view('i8') == iNaT
83-
else:
84-
o_mask = other.view('i8') == iNaT
85-
83+
o_mask = np.array(isna(other))
8684
if o_mask.any():
8785
result[o_mask] = nat_result
8886

@@ -416,9 +414,15 @@ def _evaluate_with_timedelta_like(self, other, op):
416414
def _add_datelike(self, other):
417415
# adding a timedeltaindex to a datetimelike
418416
from pandas import Timestamp, DatetimeIndex
417+
419418
if other is NaT:
420419
# GH#19124 pd.NaT is treated like a timedelta
421420
return self._nat_new()
421+
elif isinstance(other, (DatetimeIndex, np.ndarray)):
422+
# if other is an ndarray, we assume it is datetime64-dtype
423+
# defer to implementation in DatetimeIndex
424+
other = DatetimeIndex(other)
425+
return other + self
422426
else:
423427
other = Timestamp(other)
424428
i8 = self.asi8
@@ -434,7 +438,8 @@ def _sub_datelike(self, other):
434438
if other is NaT:
435439
return self._nat_new()
436440
else:
437-
raise TypeError("cannot subtract a datelike from a TimedeltaIndex")
441+
raise TypeError("cannot subtract a datelike from a {cls}"
442+
.format(cls=type(self).__name__))
438443

439444
def _addsub_offset_array(self, other, op):
440445
# Add or subtract Array-like of DateOffset objects
@@ -962,8 +967,7 @@ def _is_convertible_to_index(other):
962967

963968

964969
def _is_convertible_to_td(key):
965-
# TODO: Not all DateOffset objects are convertible to Timedelta
966-
return isinstance(key, (DateOffset, timedelta, Timedelta,
970+
return isinstance(key, (Tick, timedelta,
967971
np.timedelta64, compat.string_types))
968972

969973

pandas/tests/indexes/datetimes/test_arithmetic.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def test_dti_sub_tdi(self, tz):
508508
result = dti - tdi
509509
tm.assert_index_equal(result, expected)
510510

511-
msg = 'cannot subtract TimedeltaIndex and DatetimeIndex'
511+
msg = 'cannot subtract .*TimedeltaIndex'
512512
with tm.assert_raises_regex(TypeError, msg):
513513
tdi - dti
514514

@@ -531,7 +531,7 @@ def test_dti_isub_tdi(self, tz):
531531
result -= tdi
532532
tm.assert_index_equal(result, expected)
533533

534-
msg = 'cannot subtract TimedeltaIndex and DatetimeIndex'
534+
msg = 'cannot subtract .*TimedeltaIndex'
535535
with tm.assert_raises_regex(TypeError, msg):
536536
tdi -= dti
537537

pandas/tests/indexes/timedeltas/test_arithmetic.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ def test_addition_ops(self):
792792
pytest.raises(ValueError, lambda: tdi[0:1] + dti)
793793

794794
# random indexes
795-
pytest.raises(TypeError, lambda: tdi + Int64Index([1, 2, 3]))
795+
pytest.raises(NullFrequencyError, lambda: tdi + Int64Index([1, 2, 3]))
796796

797797
# this is a union!
798798
# pytest.raises(TypeError, lambda : Int64Index([1,2,3]) + tdi)

0 commit comments

Comments
 (0)