diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 4c6effc65a4d3..e673bfe411cb4 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -37,7 +37,7 @@ is_period_dtype, is_timedelta64_dtype) from pandas.core.dtypes.generic import ( - ABCIndex, ABCSeries, ABCPeriodIndex, ABCIndexClass) + ABCIndex, ABCSeries, ABCDataFrame, ABCPeriodIndex, ABCIndexClass) from pandas.core.dtypes.missing import isna from pandas.core import common as com, algorithms, ops from pandas.core.algorithms import checked_add_with_arr @@ -48,6 +48,7 @@ from pandas.util._decorators import Appender, cache_readonly import pandas.core.dtypes.concat as _concat import pandas.tseries.frequencies as frequencies +from pandas.tseries.offsets import Tick, DateOffset import pandas.core.indexes.base as ibase _index_doc_kwargs = dict(ibase._index_doc_kwargs) @@ -666,6 +667,9 @@ def _sub_nat(self): def _sub_period(self, other): return NotImplemented + def _add_offset(self, offset): + raise com.AbstractMethodError(self) + def _addsub_offset_array(self, other, op): """ Add or subtract array-like of DateOffset objects @@ -705,14 +709,17 @@ def __add__(self, other): from pandas import DateOffset other = lib.item_from_zerodim(other) - if isinstance(other, ABCSeries): + if isinstance(other, (ABCSeries, ABCDataFrame)): return NotImplemented # scalar others elif other is NaT: result = self._add_nat() - elif isinstance(other, (DateOffset, timedelta, np.timedelta64)): + elif isinstance(other, (Tick, timedelta, np.timedelta64)): result = self._add_delta(other) + elif isinstance(other, DateOffset): + # specifically _not_ a Tick + result = self._add_offset(other) elif isinstance(other, (datetime, np.datetime64)): result = self._add_datelike(other) elif is_integer(other): @@ -733,6 +740,12 @@ def __add__(self, other): elif is_integer_dtype(other) and self.freq is None: # GH#19123 raise NullFrequencyError("Cannot shift with no freq") + elif is_float_dtype(other): + # Explicitly catch invalid dtypes + raise TypeError("cannot add {dtype}-dtype to {cls}" + .format(dtype=other.dtype, + cls=type(self).__name__)) + else: # pragma: no cover return NotImplemented @@ -753,17 +766,20 @@ def __radd__(self, other): cls.__radd__ = __radd__ def __sub__(self, other): - from pandas import Index, DateOffset + from pandas import Index other = lib.item_from_zerodim(other) - if isinstance(other, ABCSeries): + if isinstance(other, (ABCSeries, ABCDataFrame)): return NotImplemented # scalar others elif other is NaT: result = self._sub_nat() - elif isinstance(other, (DateOffset, timedelta, np.timedelta64)): + elif isinstance(other, (Tick, timedelta, np.timedelta64)): result = self._add_delta(-other) + elif isinstance(other, DateOffset): + # specifically _not_ a Tick + result = self._add_offset(-other) elif isinstance(other, (datetime, np.datetime64)): result = self._sub_datelike(other) elif is_integer(other): @@ -790,6 +806,12 @@ def __sub__(self, other): elif is_integer_dtype(other) and self.freq is None: # GH#19123 raise NullFrequencyError("Cannot shift with no freq") + + elif is_float_dtype(other): + # Explicitly catch invalid dtypes + raise TypeError("cannot subtract {dtype}-dtype from {cls}" + .format(dtype=other.dtype, + cls=type(self).__name__)) else: # pragma: no cover return NotImplemented diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index eb8133a1bbf97..f8eea7bf1316e 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -933,8 +933,6 @@ def _add_delta(self, delta): if not isinstance(delta, TimedeltaIndex): delta = TimedeltaIndex(delta) new_values = self._add_delta_tdi(delta) - elif isinstance(delta, DateOffset): - new_values = self._add_offset(delta).asi8 else: new_values = self.astype('O') + delta @@ -945,6 +943,7 @@ def _add_delta(self, delta): return result def _add_offset(self, offset): + assert not isinstance(offset, Tick) try: if self.tz is not None: values = self.tz_localize(None) @@ -953,12 +952,13 @@ def _add_offset(self, offset): result = offset.apply_index(values) if self.tz is not None: result = result.tz_localize(self.tz) - return result except NotImplementedError: warnings.warn("Non-vectorized DateOffset being applied to Series " "or DatetimeIndex", PerformanceWarning) - return self.astype('O') + offset + result = self.astype('O') + offset + + return DatetimeIndex(result, freq='infer') def _format_native_types(self, na_rep='NaT', date_format=None, **kwargs): from pandas.io.formats.format import _get_format_datetime64_from_values diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b936a4e26af60..97cb3fbd877dd 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -21,10 +21,11 @@ import pandas.tseries.frequencies as frequencies from pandas.tseries.frequencies import get_freq_code as _gfc +from pandas.tseries.offsets import Tick, DateOffset + from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin from pandas.core.tools.datetimes import parse_time_string -import pandas.tseries.offsets as offsets from pandas._libs.lib import infer_dtype from pandas._libs import tslib, index as libindex @@ -680,9 +681,9 @@ def to_timestamp(self, freq=None, how='start'): def _maybe_convert_timedelta(self, other): if isinstance( - other, (timedelta, np.timedelta64, offsets.Tick, np.ndarray)): + other, (timedelta, np.timedelta64, Tick, np.ndarray)): offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(offset, offsets.Tick): + if isinstance(offset, Tick): if isinstance(other, np.ndarray): nanos = np.vectorize(delta_to_nanoseconds)(other) else: @@ -691,7 +692,7 @@ def _maybe_convert_timedelta(self, other): check = np.all(nanos % offset_nanos == 0) if check: return nanos // offset_nanos - elif isinstance(other, offsets.DateOffset): + elif isinstance(other, DateOffset): freqstr = other.rule_code base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: @@ -707,6 +708,30 @@ def _maybe_convert_timedelta(self, other): msg = "Input has different freq from PeriodIndex(freq={0})" raise IncompatibleFrequency(msg.format(self.freqstr)) + def _add_offset(self, other): + assert not isinstance(other, Tick) + base = frequencies.get_base_alias(other.rule_code) + if base != self.freq.rule_code: + msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) + raise IncompatibleFrequency(msg) + return self.shift(other.n) + + def _add_delta_td(self, other): + assert isinstance(other, (timedelta, np.timedelta64, Tick)) + nanos = delta_to_nanoseconds(other) + own_offset = frequencies.to_offset(self.freq.rule_code) + + if isinstance(own_offset, Tick): + offset_nanos = delta_to_nanoseconds(own_offset) + if np.all(nanos % offset_nanos == 0): + return self.shift(nanos // offset_nanos) + + # raise when input doesn't have freq + raise IncompatibleFrequency("Input has different freq from " + "{cls}(freq={freqstr})" + .format(cls=type(self).__name__, + freqstr=self.freqstr)) + def _add_delta(self, other): ordinal_delta = self._maybe_convert_timedelta(other) return self.shift(ordinal_delta) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index c42c0656c585a..a14de18b1012f 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -353,6 +353,12 @@ def _maybe_update_attributes(self, attrs): attrs['freq'] = 'infer' return attrs + def _add_offset(self, other): + assert not isinstance(other, Tick) + raise TypeError("cannot add the type {typ} to a {cls}" + .format(typ=type(other).__name__, + cls=type(self).__name__)) + def _add_delta(self, delta): """ Add a timedelta-like, Tick, or TimedeltaIndex-like object diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 0c56c6b16fb2f..8f259a7e78897 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -14,6 +14,7 @@ from pandas import (Timestamp, Timedelta, Series, DatetimeIndex, TimedeltaIndex, date_range) +from pandas.core import ops from pandas._libs import tslib from pandas._libs.tslibs.offsets import shift_months @@ -307,6 +308,17 @@ def test_dti_cmp_list(self): class TestDatetimeIndexArithmetic(object): + # ------------------------------------------------------------- + # Invalid Operations + + @pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])]) + @pytest.mark.parametrize('op', [operator.add, ops.radd, + operator.sub, ops.rsub]) + def test_dti_add_sub_float(self, op, other): + dti = DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D') + with pytest.raises(TypeError): + op(dti, other) + def test_dti_add_timestamp_raises(self): idx = DatetimeIndex(['2011-01-01', '2011-01-02']) msg = "cannot add DatetimeIndex and Timestamp" diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index d7bf1e0210f62..c75fdd35a974c 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from datetime import timedelta +import operator + import pytest import numpy as np @@ -9,6 +11,7 @@ period_range, Period, PeriodIndex, _np_version_under1p10) import pandas.core.indexes.period as period +from pandas.core import ops from pandas.errors import PerformanceWarning @@ -256,6 +259,18 @@ def test_comp_nat(self, dtype): class TestPeriodIndexArithmetic(object): + # ------------------------------------------------------------- + # Invalid Operations + + @pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])]) + @pytest.mark.parametrize('op', [operator.add, ops.radd, + operator.sub, ops.rsub]) + def test_pi_add_sub_float(self, op, other): + dti = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D') + pi = dti.to_period('D') + with pytest.raises(TypeError): + op(pi, other) + # ----------------------------------------------------------------- # __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64] diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index 9ffffb6ff06d5..9035434046ccb 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import operator + import pytest import numpy as np from datetime import timedelta @@ -11,6 +13,7 @@ Series, Timestamp, Timedelta) from pandas.errors import PerformanceWarning, NullFrequencyError +from pandas.core import ops @pytest.fixture(params=[pd.offsets.Hour(2), timedelta(hours=2), @@ -270,6 +273,15 @@ class TestTimedeltaIndexArithmetic(object): # ------------------------------------------------------------- # Invalid Operations + @pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])]) + @pytest.mark.parametrize('op', [operator.add, ops.radd, + operator.sub, ops.rsub]) + def test_tdi_add_sub_float(self, op, other): + dti = DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D') + tdi = dti - dti.shift(1) + with pytest.raises(TypeError): + op(tdi, other) + def test_tdi_add_str_invalid(self): # GH 13624 tdi = TimedeltaIndex(['1 day', '2 days'])