Skip to content

Commit 0ca77b3

Browse files
jbrockmendeljreback
authored andcommitted
Datetimelike add/sub catch cases more explicitly, tests (pandas-dev#19912)
1 parent 0038bad commit 0ca77b3

File tree

7 files changed

+106
-14
lines changed

7 files changed

+106
-14
lines changed

pandas/core/indexes/datetimelike.py

+28-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
is_period_dtype,
3838
is_timedelta64_dtype)
3939
from pandas.core.dtypes.generic import (
40-
ABCIndex, ABCSeries, ABCPeriodIndex, ABCIndexClass)
40+
ABCIndex, ABCSeries, ABCDataFrame, ABCPeriodIndex, ABCIndexClass)
4141
from pandas.core.dtypes.missing import isna
4242
from pandas.core import common as com, algorithms, ops
4343
from pandas.core.algorithms import checked_add_with_arr
@@ -48,6 +48,7 @@
4848
from pandas.util._decorators import Appender, cache_readonly
4949
import pandas.core.dtypes.concat as _concat
5050
import pandas.tseries.frequencies as frequencies
51+
from pandas.tseries.offsets import Tick, DateOffset
5152

5253
import pandas.core.indexes.base as ibase
5354
_index_doc_kwargs = dict(ibase._index_doc_kwargs)
@@ -666,6 +667,9 @@ def _sub_nat(self):
666667
def _sub_period(self, other):
667668
return NotImplemented
668669

670+
def _add_offset(self, offset):
671+
raise com.AbstractMethodError(self)
672+
669673
def _addsub_offset_array(self, other, op):
670674
"""
671675
Add or subtract array-like of DateOffset objects
@@ -705,14 +709,17 @@ def __add__(self, other):
705709
from pandas import DateOffset
706710

707711
other = lib.item_from_zerodim(other)
708-
if isinstance(other, ABCSeries):
712+
if isinstance(other, (ABCSeries, ABCDataFrame)):
709713
return NotImplemented
710714

711715
# scalar others
712716
elif other is NaT:
713717
result = self._add_nat()
714-
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
718+
elif isinstance(other, (Tick, timedelta, np.timedelta64)):
715719
result = self._add_delta(other)
720+
elif isinstance(other, DateOffset):
721+
# specifically _not_ a Tick
722+
result = self._add_offset(other)
716723
elif isinstance(other, (datetime, np.datetime64)):
717724
result = self._add_datelike(other)
718725
elif is_integer(other):
@@ -733,6 +740,12 @@ def __add__(self, other):
733740
elif is_integer_dtype(other) and self.freq is None:
734741
# GH#19123
735742
raise NullFrequencyError("Cannot shift with no freq")
743+
elif is_float_dtype(other):
744+
# Explicitly catch invalid dtypes
745+
raise TypeError("cannot add {dtype}-dtype to {cls}"
746+
.format(dtype=other.dtype,
747+
cls=type(self).__name__))
748+
736749
else: # pragma: no cover
737750
return NotImplemented
738751

@@ -753,17 +766,20 @@ def __radd__(self, other):
753766
cls.__radd__ = __radd__
754767

755768
def __sub__(self, other):
756-
from pandas import Index, DateOffset
769+
from pandas import Index
757770

758771
other = lib.item_from_zerodim(other)
759-
if isinstance(other, ABCSeries):
772+
if isinstance(other, (ABCSeries, ABCDataFrame)):
760773
return NotImplemented
761774

762775
# scalar others
763776
elif other is NaT:
764777
result = self._sub_nat()
765-
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
778+
elif isinstance(other, (Tick, timedelta, np.timedelta64)):
766779
result = self._add_delta(-other)
780+
elif isinstance(other, DateOffset):
781+
# specifically _not_ a Tick
782+
result = self._add_offset(-other)
767783
elif isinstance(other, (datetime, np.datetime64)):
768784
result = self._sub_datelike(other)
769785
elif is_integer(other):
@@ -790,6 +806,12 @@ def __sub__(self, other):
790806
elif is_integer_dtype(other) and self.freq is None:
791807
# GH#19123
792808
raise NullFrequencyError("Cannot shift with no freq")
809+
810+
elif is_float_dtype(other):
811+
# Explicitly catch invalid dtypes
812+
raise TypeError("cannot subtract {dtype}-dtype from {cls}"
813+
.format(dtype=other.dtype,
814+
cls=type(self).__name__))
793815
else: # pragma: no cover
794816
return NotImplemented
795817

pandas/core/indexes/datetimes.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -932,8 +932,6 @@ def _add_delta(self, delta):
932932
if not isinstance(delta, TimedeltaIndex):
933933
delta = TimedeltaIndex(delta)
934934
new_values = self._add_delta_tdi(delta)
935-
elif isinstance(delta, DateOffset):
936-
new_values = self._add_offset(delta).asi8
937935
else:
938936
new_values = self.astype('O') + delta
939937

@@ -944,6 +942,7 @@ def _add_delta(self, delta):
944942
return result
945943

946944
def _add_offset(self, offset):
945+
assert not isinstance(offset, Tick)
947946
try:
948947
if self.tz is not None:
949948
values = self.tz_localize(None)
@@ -952,12 +951,13 @@ def _add_offset(self, offset):
952951
result = offset.apply_index(values)
953952
if self.tz is not None:
954953
result = result.tz_localize(self.tz)
955-
return result
956954

957955
except NotImplementedError:
958956
warnings.warn("Non-vectorized DateOffset being applied to Series "
959957
"or DatetimeIndex", PerformanceWarning)
960-
return self.astype('O') + offset
958+
result = self.astype('O') + offset
959+
960+
return DatetimeIndex(result, freq='infer')
961961

962962
def _format_native_types(self, na_rep='NaT', date_format=None, **kwargs):
963963
from pandas.io.formats.format import _get_format_datetime64_from_values

pandas/core/indexes/period.py

+29-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121

2222
import pandas.tseries.frequencies as frequencies
2323
from pandas.tseries.frequencies import get_freq_code as _gfc
24+
from pandas.tseries.offsets import Tick, DateOffset
25+
2426
from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index
2527
from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin
2628
from pandas.core.tools.datetimes import parse_time_string
27-
import pandas.tseries.offsets as offsets
2829

2930
from pandas._libs.lib import infer_dtype
3031
from pandas._libs import tslib, index as libindex
@@ -680,9 +681,9 @@ def to_timestamp(self, freq=None, how='start'):
680681

681682
def _maybe_convert_timedelta(self, other):
682683
if isinstance(
683-
other, (timedelta, np.timedelta64, offsets.Tick, np.ndarray)):
684+
other, (timedelta, np.timedelta64, Tick, np.ndarray)):
684685
offset = frequencies.to_offset(self.freq.rule_code)
685-
if isinstance(offset, offsets.Tick):
686+
if isinstance(offset, Tick):
686687
if isinstance(other, np.ndarray):
687688
nanos = np.vectorize(delta_to_nanoseconds)(other)
688689
else:
@@ -691,7 +692,7 @@ def _maybe_convert_timedelta(self, other):
691692
check = np.all(nanos % offset_nanos == 0)
692693
if check:
693694
return nanos // offset_nanos
694-
elif isinstance(other, offsets.DateOffset):
695+
elif isinstance(other, DateOffset):
695696
freqstr = other.rule_code
696697
base = frequencies.get_base_alias(freqstr)
697698
if base == self.freq.rule_code:
@@ -707,6 +708,30 @@ def _maybe_convert_timedelta(self, other):
707708
msg = "Input has different freq from PeriodIndex(freq={0})"
708709
raise IncompatibleFrequency(msg.format(self.freqstr))
709710

711+
def _add_offset(self, other):
712+
assert not isinstance(other, Tick)
713+
base = frequencies.get_base_alias(other.rule_code)
714+
if base != self.freq.rule_code:
715+
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
716+
raise IncompatibleFrequency(msg)
717+
return self.shift(other.n)
718+
719+
def _add_delta_td(self, other):
720+
assert isinstance(other, (timedelta, np.timedelta64, Tick))
721+
nanos = delta_to_nanoseconds(other)
722+
own_offset = frequencies.to_offset(self.freq.rule_code)
723+
724+
if isinstance(own_offset, Tick):
725+
offset_nanos = delta_to_nanoseconds(own_offset)
726+
if np.all(nanos % offset_nanos == 0):
727+
return self.shift(nanos // offset_nanos)
728+
729+
# raise when input doesn't have freq
730+
raise IncompatibleFrequency("Input has different freq from "
731+
"{cls}(freq={freqstr})"
732+
.format(cls=type(self).__name__,
733+
freqstr=self.freqstr))
734+
710735
def _add_delta(self, other):
711736
ordinal_delta = self._maybe_convert_timedelta(other)
712737
return self.shift(ordinal_delta)

pandas/core/indexes/timedeltas.py

+6
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ def _maybe_update_attributes(self, attrs):
353353
attrs['freq'] = 'infer'
354354
return attrs
355355

356+
def _add_offset(self, other):
357+
assert not isinstance(other, Tick)
358+
raise TypeError("cannot add the type {typ} to a {cls}"
359+
.format(typ=type(other).__name__,
360+
cls=type(self).__name__))
361+
356362
def _add_delta(self, delta):
357363
"""
358364
Add a timedelta-like, Tick, or TimedeltaIndex-like object

pandas/tests/indexes/datetimes/test_arithmetic.py

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pandas import (Timestamp, Timedelta, Series,
1515
DatetimeIndex, TimedeltaIndex,
1616
date_range)
17+
from pandas.core import ops
1718
from pandas._libs import tslib
1819
from pandas._libs.tslibs.offsets import shift_months
1920

@@ -307,6 +308,17 @@ def test_dti_cmp_list(self):
307308

308309
class TestDatetimeIndexArithmetic(object):
309310

311+
# -------------------------------------------------------------
312+
# Invalid Operations
313+
314+
@pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])])
315+
@pytest.mark.parametrize('op', [operator.add, ops.radd,
316+
operator.sub, ops.rsub])
317+
def test_dti_add_sub_float(self, op, other):
318+
dti = DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D')
319+
with pytest.raises(TypeError):
320+
op(dti, other)
321+
310322
def test_dti_add_timestamp_raises(self):
311323
idx = DatetimeIndex(['2011-01-01', '2011-01-02'])
312324
msg = "cannot add DatetimeIndex and Timestamp"

pandas/tests/indexes/period/test_arithmetic.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from datetime import timedelta
3+
import operator
4+
35
import pytest
46
import numpy as np
57

@@ -9,6 +11,7 @@
911
period_range, Period, PeriodIndex,
1012
_np_version_under1p10)
1113
import pandas.core.indexes.period as period
14+
from pandas.core import ops
1215
from pandas.errors import PerformanceWarning
1316

1417

@@ -256,6 +259,18 @@ def test_comp_nat(self, dtype):
256259

257260
class TestPeriodIndexArithmetic(object):
258261

262+
# -------------------------------------------------------------
263+
# Invalid Operations
264+
265+
@pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])])
266+
@pytest.mark.parametrize('op', [operator.add, ops.radd,
267+
operator.sub, ops.rsub])
268+
def test_pi_add_sub_float(self, op, other):
269+
dti = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D')
270+
pi = dti.to_period('D')
271+
with pytest.raises(TypeError):
272+
op(pi, other)
273+
259274
# -----------------------------------------------------------------
260275
# __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64]
261276

pandas/tests/indexes/timedeltas/test_arithmetic.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
import operator
3+
24
import pytest
35
import numpy as np
46
from datetime import timedelta
@@ -11,6 +13,7 @@
1113
Series,
1214
Timestamp, Timedelta)
1315
from pandas.errors import PerformanceWarning, NullFrequencyError
16+
from pandas.core import ops
1417

1518

1619
@pytest.fixture(params=[pd.offsets.Hour(2), timedelta(hours=2),
@@ -270,6 +273,15 @@ class TestTimedeltaIndexArithmetic(object):
270273
# -------------------------------------------------------------
271274
# Invalid Operations
272275

276+
@pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])])
277+
@pytest.mark.parametrize('op', [operator.add, ops.radd,
278+
operator.sub, ops.rsub])
279+
def test_tdi_add_sub_float(self, op, other):
280+
dti = DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D')
281+
tdi = dti - dti.shift(1)
282+
with pytest.raises(TypeError):
283+
op(tdi, other)
284+
273285
def test_tdi_add_str_invalid(self):
274286
# GH 13624
275287
tdi = TimedeltaIndex(['1 day', '2 days'])

0 commit comments

Comments
 (0)