Skip to content

REF: implement general case DateOffset methods on BaseOffset #34099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 12, 2020
34 changes: 33 additions & 1 deletion pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ class _BaseOffset:
_day_opt = None
_attributes = frozenset(['n', 'normalize'])
_use_relativedelta = False
_adjust_dst = False
_adjust_dst = True
_deprecations = frozenset(["isAnchored", "onOffset"])
normalize = False # default for prior pickles

Expand Down Expand Up @@ -566,6 +566,26 @@ class _BaseOffset:

# ------------------------------------------------------------------

@apply_index_wraps
def apply_index(self, index):
"""
Vectorized apply of DateOffset to DatetimeIndex,
raises NotImplementedError for offsets without a
vectorized implementation.

Parameters
----------
index : DatetimeIndex

Returns
-------
DatetimeIndex
"""
raise NotImplementedError(
f"DateOffset subclass {type(self).__name__} "
"does not have a vectorized implementation"
)

def rollback(self, dt):
"""
Roll provided date backward to next offset only if not on offset.
Expand Down Expand Up @@ -599,6 +619,17 @@ class _BaseOffset:
# will raise NotImplementedError.
return get_day_of_month(other, self._day_opt)

def is_on_offset(self, dt) -> bool:
if self.normalize and not is_normalized(dt):
return False

# Default (slow) method for determining if some date is a member of the
# date range generated by this offset. Subclasses may have this
# re-implemented in a nicer way.
a = dt
b = (dt + self) - self
return a == b

# ------------------------------------------------------------------

def _validate_n(self, n):
Expand Down Expand Up @@ -712,6 +743,7 @@ cdef class _Tick(ABCTick):

# ensure that reversed-ops with numpy scalars return NotImplemented
__array_priority__ = 1000
_adjust_dst = False

def is_on_offset(self, dt) -> bool:
return True
Expand Down
66 changes: 22 additions & 44 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def __add__(date):
_params = cache_readonly(BaseOffset._params.fget)
freqstr = cache_readonly(BaseOffset.freqstr.fget)
_attributes = frozenset(["n", "normalize"] + list(liboffsets.relativedelta_kwds))
_adjust_dst = False

def __init__(self, n=1, normalize=False, **kwds):
BaseOffset.__init__(self, n, normalize)
Expand Down Expand Up @@ -228,11 +229,6 @@ def apply_index(self, i):
-------
y : DatetimeIndex
"""
if type(self) is not DateOffset:
raise NotImplementedError(
f"DateOffset subclass {type(self).__name__} "
"does not have a vectorized implementation"
)
kwds = self.kwds
relativedelta_fast = {
"years",
Expand Down Expand Up @@ -308,18 +304,17 @@ def is_on_offset(self, dt):
if self.normalize and not is_normalized(dt):
return False
# TODO, see #1395
if type(self) is DateOffset:
return True

# Default (slow) method for determining if some date is a member of the
# date range generated by this offset. Subclasses may have this
# re-implemented in a nicer way.
a = dt
b = (dt + self) - self
return a == b
return True


class SingleConstructorOffset(DateOffset):
# All DateOffset subclasses (other than Tick) subclass SingleConstructorOffset
__init__ = BaseOffset.__init__
_attributes = BaseOffset._attributes
apply_index = BaseOffset.apply_index
is_on_offset = BaseOffset.is_on_offset
_adjust_dst = True

@classmethod
def _from_name(cls, suffix=None):
# default _from_name calls cls with no args
Expand All @@ -334,7 +329,6 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset):
"""

_prefix = "B"
_adjust_dst = True
_attributes = frozenset(["n", "normalize", "offset"])

def __init__(self, n=1, normalize=False, offset=timedelta(0)):
Expand Down Expand Up @@ -441,6 +435,8 @@ def is_on_offset(self, dt: datetime) -> bool:


class BusinessHourMixin(BusinessMixin):
_adjust_dst = False

def __init__(self, start="09:00", end="17:00", offset=timedelta(0)):
# must be validated here to equality check
if not is_list_like(start):
Expand Down Expand Up @@ -912,11 +908,6 @@ def __init__(


class MonthOffset(SingleConstructorOffset):
_adjust_dst = True
_attributes = frozenset(["n", "normalize"])

__init__ = BaseOffset.__init__

def is_on_offset(self, dt: datetime) -> bool:
if self.normalize and not is_normalized(dt):
return False
Expand Down Expand Up @@ -997,8 +988,8 @@ class _CustomBusinessMonth(CustomMixin, BusinessMixin, MonthOffset):
["n", "normalize", "weekmask", "holidays", "calendar", "offset"]
)

is_on_offset = DateOffset.is_on_offset # override MonthOffset method
apply_index = DateOffset.apply_index # override MonthOffset method
is_on_offset = BaseOffset.is_on_offset # override MonthOffset method
apply_index = BaseOffset.apply_index # override MonthOffset method

def __init__(
self,
Expand Down Expand Up @@ -1082,8 +1073,7 @@ class CustomBusinessMonthBegin(_CustomBusinessMonth):
# Semi-Month Based Offset Classes


class SemiMonthOffset(DateOffset):
_adjust_dst = True
class SemiMonthOffset(SingleConstructorOffset):
_default_day_of_month = 15
_min_day_of_month = 2
_attributes = frozenset(["n", "normalize", "day_of_month"])
Expand Down Expand Up @@ -1304,7 +1294,7 @@ def _apply_index_days(self, i, roll):
# Week-Based Offset Classes


class Week(DateOffset):
class Week(SingleConstructorOffset):
"""
Weekly offset.

Expand All @@ -1314,7 +1304,6 @@ class Week(DateOffset):
Always generate specific day of week. 0 for Monday.
"""

_adjust_dst = True
_inc = timedelta(weeks=1)
_prefix = "W"
_attributes = frozenset(["n", "normalize", "weekday"])
Expand Down Expand Up @@ -1446,7 +1435,7 @@ def is_on_offset(self, dt):
return dt.day == self._get_offset_day(dt)


class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
class WeekOfMonth(_WeekOfMonthMixin, SingleConstructorOffset):
"""
Describes monthly dates like "the Tuesday of the 2nd week of each month".

Expand All @@ -1469,7 +1458,6 @@ class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
"""

_prefix = "WOM"
_adjust_dst = True
_attributes = frozenset(["n", "normalize", "week", "weekday"])

def __init__(self, n=1, normalize=False, week=0, weekday=0):
Expand Down Expand Up @@ -1516,7 +1504,7 @@ def _from_name(cls, suffix=None):
return cls(week=week, weekday=weekday)


class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
class LastWeekOfMonth(_WeekOfMonthMixin, SingleConstructorOffset):
"""
Describes monthly dates in last week of month like "the last Tuesday of
each month".
Expand All @@ -1537,7 +1525,6 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
"""

_prefix = "LWOM"
_adjust_dst = True
_attributes = frozenset(["n", "normalize", "weekday"])

def __init__(self, n=1, normalize=False, weekday=0):
Expand Down Expand Up @@ -1587,14 +1574,13 @@ def _from_name(cls, suffix=None):
# Quarter-Based Offset Classes


class QuarterOffset(DateOffset):
class QuarterOffset(SingleConstructorOffset):
"""
Quarter representation - doesn't call super.
"""

_default_startingMonth: Optional[int] = None
_from_name_startingMonth: Optional[int] = None
_adjust_dst = True
_attributes = frozenset(["n", "normalize", "startingMonth"])
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
# point. Also apply_index, is_on_offset, rule_code if
Expand Down Expand Up @@ -1706,12 +1692,11 @@ class QuarterBegin(QuarterOffset):
# Year-Based Offset Classes


class YearOffset(DateOffset):
class YearOffset(SingleConstructorOffset):
"""
DateOffset that just needs a month.
"""

_adjust_dst = True
_attributes = frozenset(["n", "normalize", "month"])

def _get_offset_day(self, other: datetime) -> int:
Expand Down Expand Up @@ -1807,7 +1792,7 @@ class YearBegin(YearOffset):
# Special Offset Classes


class FY5253(DateOffset):
class FY5253(SingleConstructorOffset):
"""
Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar.

Expand Down Expand Up @@ -1856,7 +1841,6 @@ class FY5253(DateOffset):
"""

_prefix = "RE"
_adjust_dst = True
_attributes = frozenset(["weekday", "startingMonth", "variation"])

def __init__(
Expand Down Expand Up @@ -2014,7 +1998,7 @@ def _from_name(cls, *args):
return cls(**cls._parse_suffix(*args))


class FY5253Quarter(DateOffset):
class FY5253Quarter(SingleConstructorOffset):
"""
DateOffset increments between business quarter dates
for 52-53 week fiscal year (also known as a 4-4-5 calendar).
Expand Down Expand Up @@ -2071,7 +2055,6 @@ class FY5253Quarter(DateOffset):
"""

_prefix = "REQ"
_adjust_dst = True
_attributes = frozenset(
["weekday", "startingMonth", "qtr_with_extra_week", "variation"]
)
Expand Down Expand Up @@ -2232,18 +2215,13 @@ def _from_name(cls, *args):
)


class Easter(DateOffset):
class Easter(SingleConstructorOffset):
"""
DateOffset for the Easter holiday using logic defined in dateutil.

Right now uses the revised method which is valid in years 1583-4099.
"""

_adjust_dst = True
_attributes = frozenset(["n", "normalize"])

__init__ = BaseOffset.__init__

@apply_wraps
def apply(self, other):
current_easter = easter(other.year)
Expand Down