Skip to content

Commit de6e107

Browse files
jbrockmendeljreback
authored andcommitted
Cleanup & De-duplication for custom offset classes (#18640)
1 parent 6b6cfb8 commit de6e107

File tree

2 files changed

+71
-130
lines changed

2 files changed

+71
-130
lines changed

pandas/_libs/tslibs/offsets.pyx

-21
Original file line numberDiff line numberDiff line change
@@ -307,27 +307,6 @@ class CacheableOffset(object):
307307
_cacheable = True
308308

309309

310-
class BeginMixin(object):
311-
# helper for vectorized offsets
312-
313-
def _beg_apply_index(self, i, freq):
314-
"""Offsets index to beginning of Period frequency"""
315-
316-
off = i.to_perioddelta('D')
317-
318-
base, mult = get_freq_code(freq)
319-
base_period = i.to_period(base)
320-
if self.n <= 0:
321-
# when subtracting, dates on start roll to prior
322-
roll = np.where(base_period.to_timestamp() == i - off,
323-
self.n, self.n + 1)
324-
else:
325-
roll = self.n
326-
327-
base = (base_period + roll).to_timestamp()
328-
return base + off
329-
330-
331310
class EndMixin(object):
332311
# helper for vectorized offsets
333312

pandas/tseries/offsets.py

+71-109
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,27 @@ def _from_name(cls, suffix=None):
413413
return cls()
414414

415415

416+
class _CustomMixin(object):
417+
"""
418+
Mixin for classes that define and validate calendar, holidays,
419+
and weekdays attributes
420+
"""
421+
def __init__(self, weekmask, holidays, calendar):
422+
calendar, holidays = _get_calendar(weekmask=weekmask,
423+
holidays=holidays,
424+
calendar=calendar)
425+
# Custom offset instances are identified by the
426+
# following two attributes. See DateOffset._params()
427+
# holidays, weekmask
428+
429+
# assumes self.kwds already exists
430+
self.kwds['weekmask'] = self.weekmask = weekmask
431+
self.kwds['holidays'] = self.holidays = holidays
432+
self.kwds['calendar'] = self.calendar = calendar
433+
434+
416435
class BusinessMixin(object):
417-
""" mixin to business types to provide related functions """
436+
""" Mixin to business types to provide related functions """
418437

419438
@property
420439
def offset(self):
@@ -572,9 +591,26 @@ def __init__(self, start='09:00', end='17:00', offset=timedelta(0)):
572591
kwds = {'offset': offset}
573592
self.start = kwds['start'] = _validate_business_time(start)
574593
self.end = kwds['end'] = _validate_business_time(end)
575-
self.kwds = kwds
594+
self.kwds.update(kwds)
576595
self._offset = offset
577596

597+
@cache_readonly
598+
def next_bday(self):
599+
"""used for moving to next businessday"""
600+
if self.n >= 0:
601+
nb_offset = 1
602+
else:
603+
nb_offset = -1
604+
if self._prefix.startswith('C'):
605+
# CustomBusinessHour
606+
return CustomBusinessDay(n=nb_offset,
607+
weekmask=self.weekmask,
608+
holidays=self.holidays,
609+
calendar=self.calendar)
610+
else:
611+
return BusinessDay(n=nb_offset)
612+
613+
# TODO: Cache this once offsets are immutable
578614
def _get_daytime_flag(self):
579615
if self.start == self.end:
580616
raise ValueError('start and end must not be the same')
@@ -616,6 +652,7 @@ def _prev_opening_time(self, other):
616652
return datetime(other.year, other.month, other.day,
617653
self.start.hour, self.start.minute)
618654

655+
# TODO: cache this once offsets are immutable
619656
def _get_business_hours_by_sec(self):
620657
"""
621658
Return business hours in a day by seconds.
@@ -784,19 +821,11 @@ def __init__(self, n=1, normalize=False, start='09:00',
784821
end='17:00', offset=timedelta(0)):
785822
self.n = self._validate_n(n)
786823
self.normalize = normalize
824+
self.kwds = {}
787825
super(BusinessHour, self).__init__(start=start, end=end, offset=offset)
788826

789-
@cache_readonly
790-
def next_bday(self):
791-
# used for moving to next businessday
792-
if self.n >= 0:
793-
nb_offset = 1
794-
else:
795-
nb_offset = -1
796-
return BusinessDay(n=nb_offset)
797-
798827

799-
class CustomBusinessDay(BusinessDay):
828+
class CustomBusinessDay(_CustomMixin, BusinessDay):
800829
"""
801830
DateOffset subclass representing possibly n custom business days,
802831
excluding holidays
@@ -822,19 +851,9 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
822851
self.n = self._validate_n(n)
823852
self.normalize = normalize
824853
self._offset = offset
825-
self.kwds = {}
826-
827-
calendar, holidays = _get_calendar(weekmask=weekmask,
828-
holidays=holidays,
829-
calendar=calendar)
830-
# CustomBusinessDay instances are identified by the
831-
# following two attributes. See DateOffset._params()
832-
# holidays, weekmask
854+
self.kwds = {'offset': offset}
833855

834-
self.kwds['weekmask'] = self.weekmask = weekmask
835-
self.kwds['holidays'] = self.holidays = holidays
836-
self.kwds['calendar'] = self.calendar = calendar
837-
self.kwds['offset'] = offset
856+
_CustomMixin.__init__(self, weekmask, holidays, calendar)
838857

839858
@apply_wraps
840859
def apply(self, other):
@@ -874,7 +893,8 @@ def onOffset(self, dt):
874893
return np.is_busday(day64, busdaycal=self.calendar)
875894

876895

877-
class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset):
896+
class CustomBusinessHour(_CustomMixin, BusinessHourMixin,
897+
SingleConstructorOffset):
878898
"""
879899
DateOffset subclass representing possibly n custom business days
880900
@@ -889,27 +909,11 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
889909
start='09:00', end='17:00', offset=timedelta(0)):
890910
self.n = self._validate_n(n)
891911
self.normalize = normalize
892-
super(CustomBusinessHour, self).__init__(start=start,
893-
end=end, offset=offset)
894-
895-
calendar, holidays = _get_calendar(weekmask=weekmask,
896-
holidays=holidays,
897-
calendar=calendar)
898-
self.kwds['weekmask'] = self.weekmask = weekmask
899-
self.kwds['holidays'] = self.holidays = holidays
900-
self.kwds['calendar'] = self.calendar = calendar
912+
self._offset = offset
913+
self.kwds = {'offset': offset}
901914

902-
@cache_readonly
903-
def next_bday(self):
904-
# used for moving to next businessday
905-
if self.n >= 0:
906-
nb_offset = 1
907-
else:
908-
nb_offset = -1
909-
return CustomBusinessDay(n=nb_offset,
910-
weekmask=self.weekmask,
911-
holidays=self.holidays,
912-
calendar=self.calendar)
915+
_CustomMixin.__init__(self, weekmask, holidays, calendar)
916+
BusinessHourMixin.__init__(self, start=start, end=end, offset=offset)
913917

914918

915919
# ---------------------------------------------------------------------
@@ -981,10 +985,10 @@ class BusinessMonthBegin(MonthOffset):
981985
_day_opt = 'business_start'
982986

983987

984-
class CustomBusinessMonthEnd(BusinessMixin, MonthOffset):
988+
class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset):
985989
"""
986990
DateOffset subclass representing one custom business month, incrementing
987-
between end of month dates
991+
between [BEGIN/END] of month dates
988992
989993
Parameters
990994
----------
@@ -999,27 +1003,19 @@ class CustomBusinessMonthEnd(BusinessMixin, MonthOffset):
9991003
passed to ``numpy.busdaycalendar``
10001004
calendar : pd.HolidayCalendar or np.busdaycalendar
10011005
"""
1002-
10031006
_cacheable = False
1004-
_prefix = 'CBM'
10051007

1006-
onOffset = DateOffset.onOffset # override MonthOffset method
1008+
onOffset = DateOffset.onOffset # override MonthOffset method
10071009
apply_index = DateOffset.apply_index # override MonthOffset method
10081010

10091011
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
10101012
holidays=None, calendar=None, offset=timedelta(0)):
10111013
self.n = self._validate_n(n)
10121014
self.normalize = normalize
10131015
self._offset = offset
1014-
self.kwds = {}
1016+
self.kwds = {'offset': offset}
10151017

1016-
calendar, holidays = _get_calendar(weekmask=weekmask,
1017-
holidays=holidays,
1018-
calendar=calendar)
1019-
self.kwds['weekmask'] = self.weekmask = weekmask
1020-
self.kwds['holidays'] = self.holidays = holidays
1021-
self.kwds['calendar'] = self.calendar = calendar
1022-
self.kwds['offset'] = offset
1018+
_CustomMixin.__init__(self, weekmask, holidays, calendar)
10231019

10241020
@cache_readonly
10251021
def cbday(self):
@@ -1028,7 +1024,17 @@ def cbday(self):
10281024

10291025
@cache_readonly
10301026
def m_offset(self):
1031-
return MonthEnd(n=1, normalize=self.normalize)
1027+
if self._prefix.endswith('S'):
1028+
# MonthBegin:
1029+
return MonthBegin(n=1, normalize=self.normalize)
1030+
else:
1031+
# MonthEnd
1032+
return MonthEnd(n=1, normalize=self.normalize)
1033+
1034+
1035+
class CustomBusinessMonthEnd(_CustomBusinessMonth):
1036+
__doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]', 'end')
1037+
_prefix = 'CBM'
10321038

10331039
@apply_wraps
10341040
def apply(self, other):
@@ -1054,57 +1060,10 @@ def apply(self, other):
10541060
return result
10551061

10561062

1057-
class CustomBusinessMonthBegin(BusinessMixin, MonthOffset):
1058-
"""
1059-
DateOffset subclass representing one custom business month, incrementing
1060-
between beginning of month dates
1061-
1062-
Parameters
1063-
----------
1064-
n : int, default 1
1065-
offset : timedelta, default timedelta(0)
1066-
normalize : bool, default False
1067-
Normalize start/end dates to midnight before generating date range
1068-
weekmask : str, Default 'Mon Tue Wed Thu Fri'
1069-
weekmask of valid business days, passed to ``numpy.busdaycalendar``
1070-
holidays : list
1071-
list/array of dates to exclude from the set of valid business days,
1072-
passed to ``numpy.busdaycalendar``
1073-
calendar : pd.HolidayCalendar or np.busdaycalendar
1074-
"""
1075-
1076-
_cacheable = False
1063+
class CustomBusinessMonthBegin(_CustomBusinessMonth):
1064+
__doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]', 'beginning')
10771065
_prefix = 'CBMS'
10781066

1079-
onOffset = DateOffset.onOffset # override MonthOffset method
1080-
apply_index = DateOffset.apply_index # override MonthOffset method
1081-
1082-
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
1083-
holidays=None, calendar=None, offset=timedelta(0)):
1084-
self.n = self._validate_n(n)
1085-
self.normalize = normalize
1086-
self._offset = offset
1087-
self.kwds = {}
1088-
1089-
# _get_calendar does validation and possible transformation
1090-
# of calendar and holidays.
1091-
calendar, holidays = _get_calendar(weekmask=weekmask,
1092-
holidays=holidays,
1093-
calendar=calendar)
1094-
self.kwds['calendar'] = self.calendar = calendar
1095-
self.kwds['weekmask'] = self.weekmask = weekmask
1096-
self.kwds['holidays'] = self.holidays = holidays
1097-
self.kwds['offset'] = offset
1098-
1099-
@cache_readonly
1100-
def cbday(self):
1101-
kwds = self.kwds
1102-
return CustomBusinessDay(n=self.n, normalize=self.normalize, **kwds)
1103-
1104-
@cache_readonly
1105-
def m_offset(self):
1106-
return MonthBegin(n=1, normalize=self.normalize)
1107-
11081067
@apply_wraps
11091068
def apply(self, other):
11101069
n = self.n
@@ -1707,13 +1666,16 @@ def onOffset(self, dt):
17071666
return dt.month == self.month and dt.day == self._get_offset_day(dt)
17081667

17091668
def __init__(self, n=1, normalize=False, month=None):
1669+
self.n = self._validate_n(n)
1670+
self.normalize = normalize
1671+
17101672
month = month if month is not None else self._default_month
17111673
self.month = month
17121674

17131675
if self.month < 1 or self.month > 12:
17141676
raise ValueError('Month must go from 1 to 12')
17151677

1716-
DateOffset.__init__(self, n=n, normalize=normalize, month=month)
1678+
self.kwds = {'month': month}
17171679

17181680
@classmethod
17191681
def _from_name(cls, suffix=None):

0 commit comments

Comments
 (0)