Skip to content

Commit 7c3653a

Browse files
authored
REF: implement general case DateOffset methods on BaseOffset (#34099)
1 parent 6bf76ef commit 7c3653a

File tree

2 files changed

+55
-45
lines changed

2 files changed

+55
-45
lines changed

pandas/_libs/tslibs/offsets.pyx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ class _BaseOffset:
408408
_day_opt = None
409409
_attributes = frozenset(['n', 'normalize'])
410410
_use_relativedelta = False
411-
_adjust_dst = False
411+
_adjust_dst = True
412412
_deprecations = frozenset(["isAnchored", "onOffset"])
413413
normalize = False # default for prior pickles
414414

@@ -566,6 +566,26 @@ class _BaseOffset:
566566

567567
# ------------------------------------------------------------------
568568

569+
@apply_index_wraps
570+
def apply_index(self, index):
571+
"""
572+
Vectorized apply of DateOffset to DatetimeIndex,
573+
raises NotImplementedError for offsets without a
574+
vectorized implementation.
575+
576+
Parameters
577+
----------
578+
index : DatetimeIndex
579+
580+
Returns
581+
-------
582+
DatetimeIndex
583+
"""
584+
raise NotImplementedError(
585+
f"DateOffset subclass {type(self).__name__} "
586+
"does not have a vectorized implementation"
587+
)
588+
569589
def rollback(self, dt):
570590
"""
571591
Roll provided date backward to next offset only if not on offset.
@@ -599,6 +619,17 @@ class _BaseOffset:
599619
# will raise NotImplementedError.
600620
return get_day_of_month(other, self._day_opt)
601621

622+
def is_on_offset(self, dt) -> bool:
623+
if self.normalize and not is_normalized(dt):
624+
return False
625+
626+
# Default (slow) method for determining if some date is a member of the
627+
# date range generated by this offset. Subclasses may have this
628+
# re-implemented in a nicer way.
629+
a = dt
630+
b = (dt + self) - self
631+
return a == b
632+
602633
# ------------------------------------------------------------------
603634

604635
def _validate_n(self, n):
@@ -712,6 +743,7 @@ cdef class _Tick(ABCTick):
712743

713744
# ensure that reversed-ops with numpy scalars return NotImplemented
714745
__array_priority__ = 1000
746+
_adjust_dst = False
715747

716748
def is_on_offset(self, dt) -> bool:
717749
return True

pandas/tseries/offsets.py

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def __add__(date):
176176
_params = cache_readonly(BaseOffset._params.fget)
177177
freqstr = cache_readonly(BaseOffset.freqstr.fget)
178178
_attributes = frozenset(["n", "normalize"] + list(liboffsets.relativedelta_kwds))
179+
_adjust_dst = False
179180

180181
def __init__(self, n=1, normalize=False, **kwds):
181182
BaseOffset.__init__(self, n, normalize)
@@ -228,11 +229,6 @@ def apply_index(self, i):
228229
-------
229230
y : DatetimeIndex
230231
"""
231-
if type(self) is not DateOffset:
232-
raise NotImplementedError(
233-
f"DateOffset subclass {type(self).__name__} "
234-
"does not have a vectorized implementation"
235-
)
236232
kwds = self.kwds
237233
relativedelta_fast = {
238234
"years",
@@ -308,18 +304,17 @@ def is_on_offset(self, dt):
308304
if self.normalize and not is_normalized(dt):
309305
return False
310306
# TODO, see #1395
311-
if type(self) is DateOffset:
312-
return True
313-
314-
# Default (slow) method for determining if some date is a member of the
315-
# date range generated by this offset. Subclasses may have this
316-
# re-implemented in a nicer way.
317-
a = dt
318-
b = (dt + self) - self
319-
return a == b
307+
return True
320308

321309

322310
class SingleConstructorOffset(DateOffset):
311+
# All DateOffset subclasses (other than Tick) subclass SingleConstructorOffset
312+
__init__ = BaseOffset.__init__
313+
_attributes = BaseOffset._attributes
314+
apply_index = BaseOffset.apply_index
315+
is_on_offset = BaseOffset.is_on_offset
316+
_adjust_dst = True
317+
323318
@classmethod
324319
def _from_name(cls, suffix=None):
325320
# default _from_name calls cls with no args
@@ -334,7 +329,6 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset):
334329
"""
335330

336331
_prefix = "B"
337-
_adjust_dst = True
338332
_attributes = frozenset(["n", "normalize", "offset"])
339333

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

442436

443437
class BusinessHourMixin(BusinessMixin):
438+
_adjust_dst = False
439+
444440
def __init__(self, start="09:00", end="17:00", offset=timedelta(0)):
445441
# must be validated here to equality check
446442
if not is_list_like(start):
@@ -912,11 +908,6 @@ def __init__(
912908

913909

914910
class MonthOffset(SingleConstructorOffset):
915-
_adjust_dst = True
916-
_attributes = frozenset(["n", "normalize"])
917-
918-
__init__ = BaseOffset.__init__
919-
920911
def is_on_offset(self, dt: datetime) -> bool:
921912
if self.normalize and not is_normalized(dt):
922913
return False
@@ -997,8 +988,8 @@ class _CustomBusinessMonth(CustomMixin, BusinessMixin, MonthOffset):
997988
["n", "normalize", "weekmask", "holidays", "calendar", "offset"]
998989
)
999990

1000-
is_on_offset = DateOffset.is_on_offset # override MonthOffset method
1001-
apply_index = DateOffset.apply_index # override MonthOffset method
991+
is_on_offset = BaseOffset.is_on_offset # override MonthOffset method
992+
apply_index = BaseOffset.apply_index # override MonthOffset method
1002993

1003994
def __init__(
1004995
self,
@@ -1082,8 +1073,7 @@ class CustomBusinessMonthBegin(_CustomBusinessMonth):
10821073
# Semi-Month Based Offset Classes
10831074

10841075

1085-
class SemiMonthOffset(DateOffset):
1086-
_adjust_dst = True
1076+
class SemiMonthOffset(SingleConstructorOffset):
10871077
_default_day_of_month = 15
10881078
_min_day_of_month = 2
10891079
_attributes = frozenset(["n", "normalize", "day_of_month"])
@@ -1304,7 +1294,7 @@ def _apply_index_days(self, i, roll):
13041294
# Week-Based Offset Classes
13051295

13061296

1307-
class Week(DateOffset):
1297+
class Week(SingleConstructorOffset):
13081298
"""
13091299
Weekly offset.
13101300
@@ -1314,7 +1304,6 @@ class Week(DateOffset):
13141304
Always generate specific day of week. 0 for Monday.
13151305
"""
13161306

1317-
_adjust_dst = True
13181307
_inc = timedelta(weeks=1)
13191308
_prefix = "W"
13201309
_attributes = frozenset(["n", "normalize", "weekday"])
@@ -1446,7 +1435,7 @@ def is_on_offset(self, dt):
14461435
return dt.day == self._get_offset_day(dt)
14471436

14481437

1449-
class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
1438+
class WeekOfMonth(_WeekOfMonthMixin, SingleConstructorOffset):
14501439
"""
14511440
Describes monthly dates like "the Tuesday of the 2nd week of each month".
14521441
@@ -1469,7 +1458,6 @@ class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
14691458
"""
14701459

14711460
_prefix = "WOM"
1472-
_adjust_dst = True
14731461
_attributes = frozenset(["n", "normalize", "week", "weekday"])
14741462

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

15181506

1519-
class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
1507+
class LastWeekOfMonth(_WeekOfMonthMixin, SingleConstructorOffset):
15201508
"""
15211509
Describes monthly dates in last week of month like "the last Tuesday of
15221510
each month".
@@ -1537,7 +1525,6 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
15371525
"""
15381526

15391527
_prefix = "LWOM"
1540-
_adjust_dst = True
15411528
_attributes = frozenset(["n", "normalize", "weekday"])
15421529

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

15891576

1590-
class QuarterOffset(DateOffset):
1577+
class QuarterOffset(SingleConstructorOffset):
15911578
"""
15921579
Quarter representation - doesn't call super.
15931580
"""
15941581

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

17081694

1709-
class YearOffset(DateOffset):
1695+
class YearOffset(SingleConstructorOffset):
17101696
"""
17111697
DateOffset that just needs a month.
17121698
"""
17131699

1714-
_adjust_dst = True
17151700
_attributes = frozenset(["n", "normalize", "month"])
17161701

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

18091794

1810-
class FY5253(DateOffset):
1795+
class FY5253(SingleConstructorOffset):
18111796
"""
18121797
Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar.
18131798
@@ -1856,7 +1841,6 @@ class FY5253(DateOffset):
18561841
"""
18571842

18581843
_prefix = "RE"
1859-
_adjust_dst = True
18601844
_attributes = frozenset(["weekday", "startingMonth", "variation"])
18611845

18621846
def __init__(
@@ -2014,7 +1998,7 @@ def _from_name(cls, *args):
20141998
return cls(**cls._parse_suffix(*args))
20151999

20162000

2017-
class FY5253Quarter(DateOffset):
2001+
class FY5253Quarter(SingleConstructorOffset):
20182002
"""
20192003
DateOffset increments between business quarter dates
20202004
for 52-53 week fiscal year (also known as a 4-4-5 calendar).
@@ -2071,7 +2055,6 @@ class FY5253Quarter(DateOffset):
20712055
"""
20722056

20732057
_prefix = "REQ"
2074-
_adjust_dst = True
20752058
_attributes = frozenset(
20762059
["weekday", "startingMonth", "qtr_with_extra_week", "variation"]
20772060
)
@@ -2232,18 +2215,13 @@ def _from_name(cls, *args):
22322215
)
22332216

22342217

2235-
class Easter(DateOffset):
2218+
class Easter(SingleConstructorOffset):
22362219
"""
22372220
DateOffset for the Easter holiday using logic defined in dateutil.
22382221
22392222
Right now uses the revised method which is valid in years 1583-4099.
22402223
"""
22412224

2242-
_adjust_dst = True
2243-
_attributes = frozenset(["n", "normalize"])
2244-
2245-
__init__ = BaseOffset.__init__
2246-
22472225
@apply_wraps
22482226
def apply(self, other):
22492227
current_easter = easter(other.year)

0 commit comments

Comments
 (0)