Skip to content
Merged
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,7 @@ Deprecations
- :meth:`DatetimeIndex.week` and `DatetimeIndex.weekofyear` are deprecated and will be removed in a future version, use :meth:`DatetimeIndex.isocalendar().week` instead (:issue:`33595`)
- :meth:`DatetimeArray.week` and `DatetimeArray.weekofyear` are deprecated and will be removed in a future version, use :meth:`DatetimeArray.isocalendar().week` instead (:issue:`33595`)
- :meth:`DateOffset.__call__` is deprecated and will be removed in a future version, use ``offset + other`` instead (:issue:`34171`)
- :meth:`~BusinessDay.apply_index` is deprecated and will be removed in a future version. Use ``offset + other`` instead (:issue:`34580`)
- :meth:`DataFrame.tshift` and :meth:`Series.tshift` are deprecated and will be removed in a future version, use :meth:`DataFrame.shift` and :meth:`Series.shift` instead (:issue:`11631`)
- Indexing an :class:`Index` object with a float key is deprecated, and will
raise an ``IndexError`` in the future. You can manually convert to an integer key
Expand Down
82 changes: 77 additions & 5 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,45 @@ cdef bint _is_normalized(datetime dt):
return True


def apply_wrapper_core(func, self, other):
result = func(self, other)
result = np.asarray(result)

if self.normalize:
result = normalize_i8_timestamps(result.view("i8"), None)

return result


def apply_index_wraps(func):
# Note: normally we would use `@functools.wraps(func)`, but this does
# not play nicely with cython class methods
def wrapper(self, other) -> np.ndarray:
def wrapper(self, other):
# other is a DatetimeArray
result = apply_wrapper_core(func, self, other)
result = type(other)(result)
warnings.warn("'Offset.apply_index(other)' is deprecated. "
"Use 'offset + other' instead.", FutureWarning)
return result

result = func(self, other)
result = np.asarray(result)
# do @functools.wraps(func) manually since it doesn't work on cdef funcs
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
try:
wrapper.__module__ = func.__module__
except AttributeError:
# AttributeError: 'method_descriptor' object has no
# attribute '__module__'
pass
return wrapper

if self.normalize:
result = normalize_i8_timestamps(result.view("i8"), None)

def apply_array_wraps(func):
# Note: normally we would use `@functools.wraps(func)`, but this does
# not play nicely with cython class methods
def wrapper(self, other) -> np.ndarray:
# other is a DatetimeArray
result = apply_wrapper_core(func, self, other)
return result

# do @functools.wraps(func) manually since it doesn't work on cdef funcs
Expand Down Expand Up @@ -554,6 +582,10 @@ cdef class BaseOffset:
raises NotImplementedError for offsets without a
vectorized implementation.

.. deprecated:: 1.1.0

Use ``offset + dtindex`` instead.

Parameters
----------
index : DatetimeIndex
Expand All @@ -567,6 +599,13 @@ cdef class BaseOffset:
"does not have a vectorized implementation"
)

@apply_array_wraps
def _apply_array(self, dtindex):
raise NotImplementedError(
f"DateOffset subclass {type(self).__name__} "
"does not have a vectorized implementation"
)

def rollback(self, dt) -> datetime:
"""
Roll provided date backward to next offset only if not on offset.
Expand Down Expand Up @@ -1031,6 +1070,10 @@ cdef class RelativeDeltaOffset(BaseOffset):
-------
ndarray[datetime64[ns]]
"""
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
dt64other = np.asarray(dtindex)
kwds = self.kwds
relativedelta_fast = {
Expand Down Expand Up @@ -1360,6 +1403,10 @@ cdef class BusinessDay(BusinessMixin):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
i8other = dtindex.view("i8")
return shift_bdays(i8other, self.n)

Expand Down Expand Up @@ -1843,6 +1890,10 @@ cdef class YearOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_quarters(
dtindex.view("i8"), self.n, self.month, self._day_opt, modby=12
)
Expand Down Expand Up @@ -1996,6 +2047,10 @@ cdef class QuarterOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_quarters(
dtindex.view("i8"), self.n, self.startingMonth, self._day_opt
)
Expand Down Expand Up @@ -2111,6 +2166,10 @@ cdef class MonthOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_months(dtindex.view("i8"), self.n, self._day_opt)
return shifted

Expand Down Expand Up @@ -2248,6 +2307,12 @@ cdef class SemiMonthOffset(SingleConstructorOffset):
@cython.wraparound(False)
@cython.boundscheck(False)
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
@cython.wraparound(False)
@cython.boundscheck(False)
def _apply_array(self, dtindex):
cdef:
int64_t[:] i8other = dtindex.view("i8")
Py_ssize_t i, count = len(i8other)
Expand Down Expand Up @@ -2407,6 +2472,10 @@ cdef class Week(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
if self.weekday is None:
td = timedelta(days=7 * self.n)
td64 = np.timedelta64(td, "ns")
Expand Down Expand Up @@ -3185,6 +3254,9 @@ cdef class CustomBusinessDay(BusinessDay):
def apply_index(self, dtindex):
raise NotImplementedError

def _apply_array(self, dtindex):
raise NotImplementedError

def is_on_offset(self, dt: datetime) -> bool:
if self.normalize and not _is_normalized(dt):
return False
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ def _add_offset(self, offset):
values = self.tz_localize(None)
else:
values = self
result = offset.apply_index(values)
result = offset._apply_array(values)
result = DatetimeArray._simple_new(result)
result = result.tz_localize(self.tz)

Expand Down
7 changes: 6 additions & 1 deletion pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3663,14 +3663,19 @@ def test_offset(self, case):

@pytest.mark.parametrize("case", offset_cases)
def test_apply_index(self, case):
# https://github.com/pandas-dev/pandas/issues/34580
offset, cases = case
s = DatetimeIndex(cases.keys())
exp = DatetimeIndex(cases.values())

with tm.assert_produces_warning(None):
# GH#22535 check that we don't get a FutureWarning from adding
# an integer array to PeriodIndex
result = offset + s
tm.assert_index_equal(result, exp)

exp = DatetimeIndex(cases.values())
with tm.assert_produces_warning(FutureWarning):
result = offset.apply_index(s)
tm.assert_index_equal(result, exp)

on_offset_cases = [
Expand Down