diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index bb7a9a57b8a75..f414cd161e562 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -377,13 +377,15 @@ cdef class _Timestamp(datetime): neg_other = -other return self + neg_other + typ = getattr(other, '_typ', None) + # a Timestamp-DatetimeIndex -> yields a negative TimedeltaIndex - elif getattr(other, '_typ', None) == 'datetimeindex': + if typ in ('datetimeindex', 'datetimearray'): # timezone comparison is performed in DatetimeIndex._sub_datelike return -other.__sub__(self) # a Timestamp-TimedeltaIndex -> yields a negative TimedeltaIndex - elif getattr(other, '_typ', None) == 'timedeltaindex': + elif typ in ('timedeltaindex', 'timedeltaarray'): return (-other).__add__(self) elif other is NaT: diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index da26966cfa94c..83cea51cec9f6 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -7,7 +7,7 @@ import numpy as np -from pandas._libs import tslibs +from pandas._libs import algos, tslibs from pandas._libs.tslibs import NaT, Timedelta, Timestamp, iNaT from pandas._libs.tslibs.fields import get_timedelta_field from pandas._libs.tslibs.timedeltas import ( @@ -24,7 +24,7 @@ from pandas.core.dtypes.missing import isna from pandas.core import ops -from pandas.core.algorithms import checked_add_with_arr +from pandas.core.algorithms import checked_add_with_arr, unique1d import pandas.core.common as com from pandas.tseries.frequencies import to_offset @@ -162,15 +162,29 @@ def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): result._freq = freq return result - def __new__(cls, values, freq=None, dtype=_TD_DTYPE): + def __new__(cls, values, freq=None, dtype=_TD_DTYPE, copy=False): freq, freq_infer = dtl.maybe_infer_freq(freq) - values = np.array(values, copy=False) - if values.dtype == np.object_: - values = array_to_timedelta64(values) + values, inferred_freq = sequence_to_td64ns( + values, copy=copy, unit=None) + if inferred_freq is not None: + if freq is not None and freq != inferred_freq: + raise ValueError('Inferred frequency {inferred} from passed ' + 'values does not conform to passed frequency ' + '{passed}' + .format(inferred=inferred_freq, + passed=freq.freqstr)) + elif freq is None: + freq = inferred_freq + freq_infer = False result = cls._simple_new(values, freq=freq) + # check that we are matching freqs + if inferred_freq is None and len(result) > 0: + if freq is not None and not freq_infer: + cls._validate_frequency(result, freq) + if freq_infer: result.freq = to_offset(result.inferred_freq) @@ -227,6 +241,21 @@ def _validate_fill_value(self, fill_value): "Got '{got}'.".format(got=fill_value)) return fill_value + # monotonicity/uniqueness properties are called via frequencies.infer_freq, + # see GH#23789 + + @property + def _is_monotonic_increasing(self): + return algos.is_monotonic(self.asi8, timelike=True)[0] + + @property + def _is_monotonic_decreasing(self): + return algos.is_monotonic(self.asi8, timelike=True)[1] + + @property + def _is_unique(self): + return len(unique1d(self.asi8)) == len(self) + # ---------------------------------------------------------------- # Arithmetic Methods @@ -283,7 +312,7 @@ def _add_datetimelike_scalar(self, other): result = checked_add_with_arr(i8, other.value, arr_mask=self._isnan) result = self._maybe_mask_results(result) - return DatetimeArrayMixin(result, tz=other.tz) + return DatetimeArrayMixin(result, tz=other.tz, freq=self.freq) def _addsub_offset_array(self, other, op): # Add or subtract Array-like of DateOffset objects diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 8b563a9b9bed0..f4b4a9933e2c9 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -1127,6 +1127,11 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): # -------------------------------------------------------------------- # Wrapping DatetimeArray + # Compat for frequency inference, see GH#23789 + _is_monotonic_increasing = Index.is_monotonic_increasing + _is_monotonic_decreasing = Index.is_monotonic_decreasing + _is_unique = Index.is_unique + _timezone = cache_readonly(DatetimeArray._timezone.fget) is_normalized = cache_readonly(DatetimeArray.is_normalized.fget) _resolution = cache_readonly(DatetimeArray._resolution.fget) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 8f50b40a20738..cb7da9129bebe 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -242,6 +242,11 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): total_seconds = wrap_array_method(TimedeltaArray.total_seconds, True) + # Compat for frequency inference, see GH#23789 + _is_monotonic_increasing = Index.is_monotonic_increasing + _is_monotonic_decreasing = Index.is_monotonic_decreasing + _is_unique = Index.is_unique + # ------------------------------------------------------------------- @Appender(_index_shared_docs['astype']) diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 873c7c92cbaf6..57cf23a39a944 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -1058,9 +1058,234 @@ def test_dti_add_tick_tzaware(self, tz_aware_fixture, box_with_array): # ------------------------------------------------------------- # RelativeDelta DateOffsets + def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array): + # GH#10699 + vec = DatetimeIndex([Timestamp('2000-01-05 00:15:00'), + Timestamp('2000-01-31 00:23:00'), + Timestamp('2000-01-01'), + Timestamp('2000-03-31'), + Timestamp('2000-02-29'), + Timestamp('2000-12-31'), + Timestamp('2000-05-15'), + Timestamp('2001-06-15')]) + vec = tm.box_expected(vec, box_with_array) + vec_items = vec.squeeze() if box_with_array is pd.DataFrame else vec + + # DateOffset relativedelta fastpath + relative_kwargs = [('years', 2), ('months', 5), ('days', 3), + ('hours', 5), ('minutes', 10), ('seconds', 2), + ('microseconds', 5)] + for i, kwd in enumerate(relative_kwargs): + off = pd.DateOffset(**dict([kwd])) + + expected = DatetimeIndex([x + off for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec + off) + + expected = DatetimeIndex([x - off for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec - off) + + off = pd.DateOffset(**dict(relative_kwargs[:i + 1])) + + expected = DatetimeIndex([x + off for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec + off) + + expected = DatetimeIndex([x - off for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec - off) + + with pytest.raises(TypeError): + off - vec + # ------------------------------------------------------------- # Non-Tick, Non-RelativeDelta DateOffsets + # TODO: redundant with test_dt64arr_add_sub_DateOffset? that includes + # tz-aware cases which this does not + @pytest.mark.parametrize('cls_and_kwargs', [ + 'YearBegin', ('YearBegin', {'month': 5}), + 'YearEnd', ('YearEnd', {'month': 5}), + 'MonthBegin', 'MonthEnd', + 'SemiMonthEnd', 'SemiMonthBegin', + 'Week', ('Week', {'weekday': 3}), + 'Week', ('Week', {'weekday': 6}), + 'BusinessDay', 'BDay', 'QuarterEnd', 'QuarterBegin', + 'CustomBusinessDay', 'CDay', 'CBMonthEnd', + 'CBMonthBegin', 'BMonthBegin', 'BMonthEnd', + 'BusinessHour', 'BYearBegin', 'BYearEnd', + 'BQuarterBegin', ('LastWeekOfMonth', {'weekday': 2}), + ('FY5253Quarter', {'qtr_with_extra_week': 1, + 'startingMonth': 1, + 'weekday': 2, + 'variation': 'nearest'}), + ('FY5253', {'weekday': 0, 'startingMonth': 2, 'variation': 'nearest'}), + ('WeekOfMonth', {'weekday': 2, 'week': 2}), + 'Easter', ('DateOffset', {'day': 4}), + ('DateOffset', {'month': 5})]) + @pytest.mark.parametrize('normalize', [True, False]) + @pytest.mark.parametrize('n', [0, 5]) + def test_dt64arr_add_sub_DateOffsets(self, box_with_array, + n, normalize, cls_and_kwargs): + # GH#10699 + # assert vectorized operation matches pointwise operations + + if isinstance(cls_and_kwargs, tuple): + # If cls_name param is a tuple, then 2nd entry is kwargs for + # the offset constructor + cls_name, kwargs = cls_and_kwargs + else: + cls_name = cls_and_kwargs + kwargs = {} + + if n == 0 and cls_name in ['WeekOfMonth', 'LastWeekOfMonth', + 'FY5253Quarter', 'FY5253']: + # passing n = 0 is invalid for these offset classes + return + + vec = DatetimeIndex([Timestamp('2000-01-05 00:15:00'), + Timestamp('2000-01-31 00:23:00'), + Timestamp('2000-01-01'), + Timestamp('2000-03-31'), + Timestamp('2000-02-29'), + Timestamp('2000-12-31'), + Timestamp('2000-05-15'), + Timestamp('2001-06-15')]) + vec = tm.box_expected(vec, box_with_array) + vec_items = vec.squeeze() if box_with_array is pd.DataFrame else vec + + offset_cls = getattr(pd.offsets, cls_name) + + with warnings.catch_warnings(record=True): + # pandas.errors.PerformanceWarning: Non-vectorized DateOffset being + # applied to Series or DatetimeIndex + # we aren't testing that here, so ignore. + warnings.simplefilter("ignore", PerformanceWarning) + + offset = offset_cls(n, normalize=normalize, **kwargs) + + expected = DatetimeIndex([x + offset for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec + offset) + + expected = DatetimeIndex([x - offset for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, vec - offset) + + expected = DatetimeIndex([offset + x for x in vec_items]) + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(expected, offset + vec) + + with pytest.raises(TypeError): + offset - vec + + def test_dt64arr_add_sub_DateOffset(self, box_with_array): + # GH#10699 + s = date_range('2000-01-01', '2000-01-31', name='a') + s = tm.box_expected(s, box_with_array) + result = s + pd.DateOffset(years=1) + result2 = pd.DateOffset(years=1) + s + exp = date_range('2001-01-01', '2001-01-31', name='a') + exp = tm.box_expected(exp, box_with_array) + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + result = s - pd.DateOffset(years=1) + exp = date_range('1999-01-01', '1999-01-31', name='a') + exp = tm.box_expected(exp, box_with_array) + tm.assert_equal(result, exp) + + s = DatetimeIndex([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + # FIXME: ValueError with tzaware DataFrame transpose + s = tm.box_expected(s, box_with_array, transpose=False) + result = s + pd.offsets.Day() + result2 = pd.offsets.Day() + s + exp = DatetimeIndex([Timestamp('2000-01-16 00:15:00', tz='US/Central'), + Timestamp('2000-02-16', tz='US/Central')], + name='a') + exp = tm.box_expected(exp, box_with_array, transpose=False) + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + s = DatetimeIndex([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + s = tm.box_expected(s, box_with_array, transpose=False) + result = s + pd.offsets.MonthEnd() + result2 = pd.offsets.MonthEnd() + s + exp = DatetimeIndex([Timestamp('2000-01-31 00:15:00', tz='US/Central'), + Timestamp('2000-02-29', tz='US/Central')], + name='a') + exp = tm.box_expected(exp, box_with_array, transpose=False) + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + # TODO: __sub__, __rsub__ + def test_dt64arr_add_mixed_offset_array(self, box_with_array): + # GH#10699 + # array of offsets + s = DatetimeIndex([Timestamp('2000-1-1'), Timestamp('2000-2-1')]) + s = tm.box_expected(s, box_with_array) + + warn = None if box_with_array is pd.DataFrame else PerformanceWarning + with tm.assert_produces_warning(warn, + clear=[pd.core.arrays.datetimelike]): + other = pd.Index([pd.offsets.DateOffset(years=1), + pd.offsets.MonthEnd()]) + other = tm.box_expected(other, box_with_array) + result = s + other + exp = DatetimeIndex([Timestamp('2001-1-1'), + Timestamp('2000-2-29')]) + exp = tm.box_expected(exp, box_with_array) + tm.assert_equal(result, exp) + + # same offset + other = pd.Index([pd.offsets.DateOffset(years=1), + pd.offsets.DateOffset(years=1)]) + other = tm.box_expected(other, box_with_array) + result = s + other + exp = DatetimeIndex([Timestamp('2001-1-1'), + Timestamp('2001-2-1')]) + exp = tm.box_expected(exp, box_with_array) + tm.assert_equal(result, exp) + + # TODO: overlap with test_dt64arr_add_mixed_offset_array? + def test_dt64arr_add_sub_offset_ndarray(self, tz_naive_fixture, + box_with_array): + # GH#18849 + if box_with_array is pd.DataFrame: + pytest.xfail("FIXME: ValueError with transpose; " + "alignment error without") + + tz = tz_naive_fixture + dti = pd.date_range('2017-01-01', periods=2, tz=tz) + dtarr = tm.box_expected(dti, box_with_array) + + other = np.array([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) + + warn = None if box_with_array is pd.DataFrame else PerformanceWarning + with tm.assert_produces_warning(warn, + clear=[pd.core.arrays.datetimelike]): + res = dtarr + other + expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))], + name=dti.name, freq='infer') + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(res, expected) + + with tm.assert_produces_warning(warn, + clear=[pd.core.arrays.datetimelike]): + res2 = other + dtarr + tm.assert_equal(res2, expected) + + with tm.assert_produces_warning(warn, + clear=[pd.core.arrays.datetimelike]): + res = dtarr - other + expected = DatetimeIndex([dti[n] - other[n] for n in range(len(dti))], + name=dti.name, freq='infer') + expected = tm.box_expected(expected, box_with_array) + tm.assert_equal(res, expected) + class TestDatetime64OverflowHandling(object): # TODO: box + de-duplicate @@ -1823,24 +2048,6 @@ def test_dti_add_series(self, tz, names): result4 = index + ser.values tm.assert_index_equal(result4, expected) - def test_dti_add_offset_array(self, tz_naive_fixture): - # GH#18849 - tz = tz_naive_fixture - dti = pd.date_range('2017-01-01', periods=2, tz=tz) - other = np.array([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) - - with tm.assert_produces_warning(PerformanceWarning, - clear=[pd.core.arrays.datetimelike]): - res = dti + other - expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))], - name=dti.name, freq='infer') - tm.assert_index_equal(res, expected) - - with tm.assert_produces_warning(PerformanceWarning, - clear=[pd.core.arrays.datetimelike]): - res2 = other + dti - tm.assert_index_equal(res2, expected) - @pytest.mark.parametrize('names', [(None, None, None), ('foo', 'bar', None), ('foo', 'foo', 'foo')]) @@ -1863,19 +2070,6 @@ def test_dti_add_offset_index(self, tz_naive_fixture, names): res2 = other + dti tm.assert_index_equal(res2, expected) - def test_dti_sub_offset_array(self, tz_naive_fixture): - # GH#18824 - tz = tz_naive_fixture - dti = pd.date_range('2017-01-01', periods=2, tz=tz) - other = np.array([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) - - with tm.assert_produces_warning(PerformanceWarning, - clear=[pd.core.arrays.datetimelike]): - res = dti - other - expected = DatetimeIndex([dti[n] - other[n] for n in range(len(dti))], - name=dti.name, freq='infer') - tm.assert_index_equal(res, expected) - @pytest.mark.parametrize('names', [(None, None, None), ('foo', 'bar', None), ('foo', 'foo', 'foo')]) @@ -1925,198 +2119,6 @@ def test_dti_with_offset_series(self, tz_naive_fixture, names): tm.assert_series_equal(res3, expected_sub) -def test_dt64_with_offset_array(box_with_array): - # GH#10699 - # array of offsets - s = DatetimeIndex([Timestamp('2000-1-1'), Timestamp('2000-2-1')]) - s = tm.box_expected(s, box_with_array) - - warn = PerformanceWarning if box_with_array is not pd.DataFrame else None - with tm.assert_produces_warning(warn, - clear=[pd.core.arrays.datetimelike]): - other = pd.Index([pd.offsets.DateOffset(years=1), - pd.offsets.MonthEnd()]) - other = tm.box_expected(other, box_with_array) - result = s + other - exp = DatetimeIndex([Timestamp('2001-1-1'), Timestamp('2000-2-29')]) - exp = tm.box_expected(exp, box_with_array) - tm.assert_equal(result, exp) - - # same offset - other = pd.Index([pd.offsets.DateOffset(years=1), - pd.offsets.DateOffset(years=1)]) - other = tm.box_expected(other, box_with_array) - result = s + other - exp = DatetimeIndex([Timestamp('2001-1-1'), Timestamp('2001-2-1')]) - exp = tm.box_expected(exp, box_with_array) - tm.assert_equal(result, exp) - - -def test_dt64_with_DateOffsets_relativedelta(box_with_array): - # GH#10699 - if box_with_array is tm.to_array: - pytest.xfail("apply_index implementations are Index-specific") - - vec = DatetimeIndex([Timestamp('2000-01-05 00:15:00'), - Timestamp('2000-01-31 00:23:00'), - Timestamp('2000-01-01'), - Timestamp('2000-03-31'), - Timestamp('2000-02-29'), - Timestamp('2000-12-31'), - Timestamp('2000-05-15'), - Timestamp('2001-06-15')]) - vec = tm.box_expected(vec, box_with_array) - vec_items = vec.squeeze() if box_with_array is pd.DataFrame else vec - - # DateOffset relativedelta fastpath - relative_kwargs = [('years', 2), ('months', 5), ('days', 3), - ('hours', 5), ('minutes', 10), ('seconds', 2), - ('microseconds', 5)] - for i, kwd in enumerate(relative_kwargs): - off = pd.DateOffset(**dict([kwd])) - - expected = DatetimeIndex([x + off for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec + off) - - expected = DatetimeIndex([x - off for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec - off) - - off = pd.DateOffset(**dict(relative_kwargs[:i + 1])) - - expected = DatetimeIndex([x + off for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec + off) - - expected = DatetimeIndex([x - off for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec - off) - - with pytest.raises(TypeError): - off - vec - - -@pytest.mark.parametrize('cls_and_kwargs', [ - 'YearBegin', ('YearBegin', {'month': 5}), - 'YearEnd', ('YearEnd', {'month': 5}), - 'MonthBegin', 'MonthEnd', - 'SemiMonthEnd', 'SemiMonthBegin', - 'Week', ('Week', {'weekday': 3}), - 'Week', ('Week', {'weekday': 6}), - 'BusinessDay', 'BDay', 'QuarterEnd', 'QuarterBegin', - 'CustomBusinessDay', 'CDay', 'CBMonthEnd', - 'CBMonthBegin', 'BMonthBegin', 'BMonthEnd', - 'BusinessHour', 'BYearBegin', 'BYearEnd', - 'BQuarterBegin', ('LastWeekOfMonth', {'weekday': 2}), - ('FY5253Quarter', {'qtr_with_extra_week': 1, - 'startingMonth': 1, - 'weekday': 2, - 'variation': 'nearest'}), - ('FY5253', {'weekday': 0, 'startingMonth': 2, 'variation': 'nearest'}), - ('WeekOfMonth', {'weekday': 2, 'week': 2}), - 'Easter', ('DateOffset', {'day': 4}), - ('DateOffset', {'month': 5})]) -@pytest.mark.parametrize('normalize', [True, False]) -def test_dt64_with_DateOffsets(box_with_array, normalize, cls_and_kwargs): - # GH#10699 - # assert these are equal on a piecewise basis - if box_with_array is tm.to_array: - pytest.xfail("apply_index implementations are Index-specific") - - vec = DatetimeIndex([Timestamp('2000-01-05 00:15:00'), - Timestamp('2000-01-31 00:23:00'), - Timestamp('2000-01-01'), - Timestamp('2000-03-31'), - Timestamp('2000-02-29'), - Timestamp('2000-12-31'), - Timestamp('2000-05-15'), - Timestamp('2001-06-15')]) - vec = tm.box_expected(vec, box_with_array) - vec_items = vec.squeeze() if box_with_array is pd.DataFrame else vec - - if isinstance(cls_and_kwargs, tuple): - # If cls_name param is a tuple, then 2nd entry is kwargs for - # the offset constructor - cls_name, kwargs = cls_and_kwargs - else: - cls_name = cls_and_kwargs - kwargs = {} - - offset_cls = getattr(pd.offsets, cls_name) - - with warnings.catch_warnings(record=True): - # pandas.errors.PerformanceWarning: Non-vectorized DateOffset being - # applied to Series or DatetimeIndex - # we aren't testing that here, so ignore. - warnings.simplefilter("ignore", PerformanceWarning) - for n in [0, 5]: - if (cls_name in ['WeekOfMonth', 'LastWeekOfMonth', - 'FY5253Quarter', 'FY5253'] and n == 0): - # passing n = 0 is invalid for these offset classes - continue - - offset = offset_cls(n, normalize=normalize, **kwargs) - - expected = DatetimeIndex([x + offset for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec + offset) - - expected = DatetimeIndex([x - offset for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, vec - offset) - - expected = DatetimeIndex([offset + x for x in vec_items]) - expected = tm.box_expected(expected, box_with_array) - tm.assert_equal(expected, offset + vec) - - with pytest.raises(TypeError): - offset - vec - - -def test_datetime64_with_DateOffset(box_with_array): - # GH#10699 - if box_with_array is tm.to_array: - pytest.xfail("DateOffset.apply_index uses _shallow_copy") - - s = date_range('2000-01-01', '2000-01-31', name='a') - s = tm.box_expected(s, box_with_array) - result = s + pd.DateOffset(years=1) - result2 = pd.DateOffset(years=1) + s - exp = date_range('2001-01-01', '2001-01-31', name='a') - exp = tm.box_expected(exp, box_with_array) - tm.assert_equal(result, exp) - tm.assert_equal(result2, exp) - - result = s - pd.DateOffset(years=1) - exp = date_range('1999-01-01', '1999-01-31', name='a') - exp = tm.box_expected(exp, box_with_array) - tm.assert_equal(result, exp) - - s = DatetimeIndex([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - # FIXME: ValueError with tzaware DataFrame transpose - s = tm.box_expected(s, box_with_array, transpose=False) - result = s + pd.offsets.Day() - result2 = pd.offsets.Day() + s - exp = DatetimeIndex([Timestamp('2000-01-16 00:15:00', tz='US/Central'), - Timestamp('2000-02-16', tz='US/Central')], name='a') - exp = tm.box_expected(exp, box_with_array, transpose=False) - tm.assert_equal(result, exp) - tm.assert_equal(result2, exp) - - s = DatetimeIndex([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - s = tm.box_expected(s, box_with_array, transpose=False) - result = s + pd.offsets.MonthEnd() - result2 = pd.offsets.MonthEnd() + s - exp = DatetimeIndex([Timestamp('2000-01-31 00:15:00', tz='US/Central'), - Timestamp('2000-02-29', tz='US/Central')], name='a') - exp = tm.box_expected(exp, box_with_array, transpose=False) - tm.assert_equal(result, exp) - tm.assert_equal(result2, exp) - - @pytest.mark.parametrize('years', [-1, 0, 1]) @pytest.mark.parametrize('months', [-2, 0, 2]) def test_shift_months(years, months): diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 58c7216f0eece..2b300cb101201 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -452,10 +452,6 @@ def test_td64arr_add_timestamp(self, box_with_array, tz_naive_fixture): def test_td64arr_add_sub_timestamp(self, box_with_array): # GH#11925 - if box_with_array is tm.to_array: - pytest.xfail("DatetimeArray.__sub__ returns ndarray instead " - "of TimedeltaArray") - ts = Timestamp('2012-01-01') # TODO: parametrize over types of datetime scalar? @@ -1105,6 +1101,7 @@ def test_tdi_rmul_arraylike(self, other, box_with_array): tdi = TimedeltaIndex(['1 Day'] * 10) expected = timedelta_range('1 days', '10 days') + expected._freq = None tdi = tm.box_expected(tdi, box) expected = tm.box_expected(expected, xbox) diff --git a/pandas/tests/indexes/timedeltas/test_construction.py b/pandas/tests/indexes/timedeltas/test_construction.py index 074c8904b55b1..ba20febfeafad 100644 --- a/pandas/tests/indexes/timedeltas/test_construction.py +++ b/pandas/tests/indexes/timedeltas/test_construction.py @@ -6,6 +6,7 @@ import pandas as pd import pandas.util.testing as tm from pandas import TimedeltaIndex, timedelta_range, to_timedelta, Timedelta +from pandas.core.arrays import TimedeltaArrayMixin as TimedeltaArray class TestTimedeltaIndex(object): @@ -41,6 +42,10 @@ def test_infer_from_tdi_mismatch(self): with pytest.raises(ValueError, match=msg): TimedeltaIndex(tdi, freq='D') + with pytest.raises(ValueError, match=msg): + # GH#23789 + TimedeltaArray(tdi, freq='D') + def test_dt64_data_invalid(self): # GH#23539 # passing tz-aware DatetimeIndex raises, naive or ndarray[datetime64] diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 97ef91a02dfb8..8cdec31d7ce8a 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -295,8 +295,8 @@ def __init__(self, index, warn=True): if len(index) < 3: raise ValueError('Need at least 3 dates to infer frequency') - self.is_monotonic = (self.index.is_monotonic_increasing or - self.index.is_monotonic_decreasing) + self.is_monotonic = (self.index._is_monotonic_increasing or + self.index._is_monotonic_decreasing) @cache_readonly def deltas(self): @@ -323,7 +323,7 @@ def get_freq(self): # noqa:F811 ------- freqstr : str or None """ - if not self.is_monotonic or not self.index.is_unique: + if not self.is_monotonic or not self.index._is_unique: return None delta = self.deltas[0] diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 2341b3a1605c0..45f10a2f06fa2 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -275,14 +275,17 @@ def apply_index(self, i): kwds.get('months', 0)) * self.n) if months: shifted = liboffsets.shift_months(i.asi8, months) - i = i._shallow_copy(shifted) + i = type(i)(shifted, freq=i.freq, dtype=i.dtype) weeks = (kwds.get('weeks', 0)) * self.n if weeks: # integer addition on PeriodIndex is deprecated, # so we directly use _time_shift instead asper = i.to_period('W') - shifted = asper._data._time_shift(weeks) + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + shifted = asper._time_shift(weeks) i = shifted.to_timestamp() + i.to_perioddelta('W') timedelta_kwds = {k: v for k, v in kwds.items() @@ -539,17 +542,21 @@ def apply_index(self, i): # to_period rolls forward to next BDay; track and # reduce n where it does when rolling forward asper = i.to_period('B') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + if self.n > 0: shifted = (i.to_perioddelta('B') - time).asi8 != 0 # Integer-array addition is deprecated, so we use # _time_shift directly roll = np.where(shifted, self.n - 1, self.n) - shifted = asper._data._addsub_int_array(roll, operator.add) + shifted = asper._addsub_int_array(roll, operator.add) else: # Integer addition is deprecated, so we use _time_shift directly roll = self.n - shifted = asper._data._time_shift(roll) + shifted = asper._time_shift(roll) result = shifted.to_timestamp() + time return result @@ -926,7 +933,9 @@ def apply(self, other): @apply_index_wraps def apply_index(self, i): shifted = liboffsets.shift_months(i.asi8, self.n, self._day_opt) - return i._shallow_copy(shifted) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(i)._simple_new(shifted, freq=i.freq, tz=i.tz) class MonthEnd(MonthOffset): @@ -1140,7 +1149,11 @@ def apply_index(self, i): # integer-array addition on PeriodIndex is deprecated, # so we use _addsub_int_array directly asper = i.to_period('M') - shifted = asper._data._addsub_int_array(roll // 2, operator.add) + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + + shifted = asper._addsub_int_array(roll // 2, operator.add) i = type(dti)(shifted.to_timestamp()) # apply the correct day @@ -1329,7 +1342,12 @@ def apply_index(self, i): if self.weekday is None: # integer addition on PeriodIndex is deprecated, # so we use _time_shift directly - shifted = i.to_period('W')._data._time_shift(self.n) + asper = i.to_period('W') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + + shifted = asper._time_shift(self.n) return shifted.to_timestamp() + i.to_perioddelta('W') else: return self._end_apply_index(i) @@ -1351,6 +1369,10 @@ def _end_apply_index(self, dtindex): base, mult = libfrequencies.get_freq_code(self.freqstr) base_period = dtindex.to_period(base) + if not isinstance(base_period._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + base_period = base_period._data + if self.n > 0: # when adding, dates on end roll to next normed = dtindex - off + Timedelta(1, 'D') - Timedelta(1, 'ns') @@ -1358,13 +1380,13 @@ def _end_apply_index(self, dtindex): self.n, self.n - 1) # integer-array addition on PeriodIndex is deprecated, # so we use _addsub_int_array directly - shifted = base_period._data._addsub_int_array(roll, operator.add) + shifted = base_period._addsub_int_array(roll, operator.add) base = shifted.to_timestamp(how='end') else: # integer addition on PeriodIndex is deprecated, # so we use _time_shift directly roll = self.n - base = base_period._data._time_shift(roll).to_timestamp(how='end') + base = base_period._time_shift(roll).to_timestamp(how='end') return base + off + Timedelta(1, 'ns') - Timedelta(1, 'D') @@ -1617,7 +1639,10 @@ def onOffset(self, dt): def apply_index(self, dtindex): shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, self.startingMonth, self._day_opt) - return dtindex._shallow_copy(shifted) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(dtindex)._simple_new(shifted, freq=dtindex.freq, + tz=dtindex.tz) class BQuarterEnd(QuarterOffset): @@ -1694,7 +1719,10 @@ def apply_index(self, dtindex): shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, self.month, self._day_opt, modby=12) - return dtindex._shallow_copy(shifted) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(dtindex)._simple_new(shifted, freq=dtindex.freq, + tz=dtindex.tz) def onOffset(self, dt): if self.normalize and not _is_normalized(dt):