Skip to content

Commit 407cc5c

Browse files
authored
REF: make YearOffset a cdef class (#34264)
1 parent aa79cbd commit 407cc5c

File tree

3 files changed

+82
-95
lines changed

3 files changed

+82
-95
lines changed

doc/source/reference/offset_frequency.rst

+4-34
Original file line numberDiff line numberDiff line change
@@ -783,40 +783,6 @@ Methods
783783
QuarterBegin.is_on_offset
784784
QuarterBegin.__call__
785785

786-
YearOffset
787-
----------
788-
.. autosummary::
789-
:toctree: api/
790-
791-
YearOffset
792-
793-
Properties
794-
~~~~~~~~~~
795-
.. autosummary::
796-
:toctree: api/
797-
798-
YearOffset.freqstr
799-
YearOffset.kwds
800-
YearOffset.name
801-
YearOffset.nanos
802-
YearOffset.normalize
803-
YearOffset.rule_code
804-
YearOffset.n
805-
806-
Methods
807-
~~~~~~~
808-
.. autosummary::
809-
:toctree: api/
810-
811-
YearOffset.apply
812-
YearOffset.apply_index
813-
YearOffset.copy
814-
YearOffset.isAnchored
815-
YearOffset.onOffset
816-
YearOffset.is_anchored
817-
YearOffset.is_on_offset
818-
YearOffset.__call__
819-
820786
BYearEnd
821787
--------
822788
.. autosummary::
@@ -836,6 +802,7 @@ Properties
836802
BYearEnd.normalize
837803
BYearEnd.rule_code
838804
BYearEnd.n
805+
BYearEnd.month
839806

840807
Methods
841808
~~~~~~~
@@ -870,6 +837,7 @@ Properties
870837
BYearBegin.normalize
871838
BYearBegin.rule_code
872839
BYearBegin.n
840+
BYearBegin.month
873841

874842
Methods
875843
~~~~~~~
@@ -904,6 +872,7 @@ Properties
904872
YearEnd.normalize
905873
YearEnd.rule_code
906874
YearEnd.n
875+
YearEnd.month
907876

908877
Methods
909878
~~~~~~~
@@ -938,6 +907,7 @@ Properties
938907
YearBegin.normalize
939908
YearBegin.rule_code
940909
YearBegin.n
910+
YearBegin.month
941911

942912
Methods
943913
~~~~~~~

pandas/_libs/tslibs/offsets.pyx

+74-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ from pandas._libs.tslibs.util cimport is_integer_object, is_datetime64_object
2424

2525
from pandas._libs.tslibs.base cimport ABCTimestamp
2626

27-
from pandas._libs.tslibs.ccalendar import MONTHS, DAYS, weekday_to_int, int_to_weekday
27+
from pandas._libs.tslibs.ccalendar import (
28+
MONTHS, DAYS, MONTH_ALIASES, MONTH_TO_CAL_NUM, weekday_to_int, int_to_weekday,
29+
)
2830
from pandas._libs.tslibs.ccalendar cimport get_days_in_month, dayofweek
2931
from pandas._libs.tslibs.conversion cimport (
3032
convert_datetime_to_tsobject,
@@ -455,6 +457,11 @@ cdef class BaseOffset:
455457
all_paras = self.__dict__.copy()
456458
all_paras["n"] = self.n
457459
all_paras["normalize"] = self.normalize
460+
for attr in self._attributes:
461+
if hasattr(self, attr) and attr not in self.__dict__:
462+
# cython attributes are not in __dict__
463+
all_paras[attr] = getattr(self, attr)
464+
458465
if 'holidays' in all_paras and not all_paras['holidays']:
459466
all_paras.pop('holidays')
460467
exclude = ['kwds', 'name', 'calendar']
@@ -551,8 +558,9 @@ cdef class BaseOffset:
551558
def _repr_attrs(self) -> str:
552559
exclude = {"n", "inc", "normalize"}
553560
attrs = []
554-
for attr in sorted(self.__dict__):
555-
if attr.startswith("_") or attr == "kwds":
561+
for attr in sorted(self._attributes):
562+
if attr.startswith("_") or attr == "kwds" or not hasattr(self, attr):
563+
# DateOffset may not have some of these attributes
556564
continue
557565
elif attr not in exclude:
558566
value = getattr(self, attr)
@@ -1152,6 +1160,69 @@ class WeekOfMonthMixin(BaseOffset):
11521160
return f"{self._prefix}-{self.week + 1}{weekday}"
11531161

11541162

1163+
# ----------------------------------------------------------------------
1164+
1165+
cdef class YearOffset(BaseOffset):
1166+
"""
1167+
DateOffset that just needs a month.
1168+
"""
1169+
_attributes = frozenset(["n", "normalize", "month"])
1170+
1171+
# _default_month: int # FIXME: python annotation here breaks things
1172+
1173+
cdef readonly:
1174+
int month
1175+
1176+
def __init__(self, n=1, normalize=False, month=None):
1177+
BaseOffset.__init__(self, n, normalize)
1178+
1179+
month = month if month is not None else self._default_month
1180+
self.month = month
1181+
1182+
if month < 1 or month > 12:
1183+
raise ValueError("Month must go from 1 to 12")
1184+
1185+
def __reduce__(self):
1186+
return type(self), (self.n, self.normalize, self.month)
1187+
1188+
@classmethod
1189+
def _from_name(cls, suffix=None):
1190+
kwargs = {}
1191+
if suffix:
1192+
kwargs["month"] = MONTH_TO_CAL_NUM[suffix]
1193+
return cls(**kwargs)
1194+
1195+
@property
1196+
def rule_code(self) -> str:
1197+
month = MONTH_ALIASES[self.month]
1198+
return f"{self._prefix}-{month}"
1199+
1200+
def is_on_offset(self, dt) -> bool:
1201+
if self.normalize and not is_normalized(dt):
1202+
return False
1203+
return dt.month == self.month and dt.day == self._get_offset_day(dt)
1204+
1205+
def _get_offset_day(self, other) -> int:
1206+
# override BaseOffset method to use self.month instead of other.month
1207+
# TODO: there may be a more performant way to do this
1208+
return get_day_of_month(
1209+
other.replace(month=self.month), self._day_opt
1210+
)
1211+
1212+
@apply_wraps
1213+
def apply(self, other):
1214+
years = roll_yearday(other, self.n, self.month, self._day_opt)
1215+
months = years * 12 + (self.month - other.month)
1216+
return shift_month(other, months, self._day_opt)
1217+
1218+
@apply_index_wraps
1219+
def apply_index(self, dtindex):
1220+
shifted = shift_quarters(
1221+
dtindex.asi8, self.n, self.month, self._day_opt, modby=12
1222+
)
1223+
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)
1224+
1225+
11551226
# ----------------------------------------------------------------------
11561227
# RelativeDelta Arithmetic
11571228

pandas/tseries/offsets.py

+4-58
Original file line numberDiff line numberDiff line change
@@ -1565,61 +1565,7 @@ class QuarterBegin(QuarterOffset):
15651565
# Year-Based Offset Classes
15661566

15671567

1568-
class YearOffset(SingleConstructorOffset):
1569-
"""
1570-
DateOffset that just needs a month.
1571-
"""
1572-
1573-
_attributes = frozenset(["n", "normalize", "month"])
1574-
1575-
def _get_offset_day(self, other: datetime) -> int:
1576-
# override BaseOffset method to use self.month instead of other.month
1577-
# TODO: there may be a more performant way to do this
1578-
return liboffsets.get_day_of_month(
1579-
other.replace(month=self.month), self._day_opt
1580-
)
1581-
1582-
@apply_wraps
1583-
def apply(self, other):
1584-
years = roll_yearday(other, self.n, self.month, self._day_opt)
1585-
months = years * 12 + (self.month - other.month)
1586-
return shift_month(other, months, self._day_opt)
1587-
1588-
@apply_index_wraps
1589-
def apply_index(self, dtindex):
1590-
shifted = liboffsets.shift_quarters(
1591-
dtindex.asi8, self.n, self.month, self._day_opt, modby=12
1592-
)
1593-
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)
1594-
1595-
def is_on_offset(self, dt: datetime) -> bool:
1596-
if self.normalize and not is_normalized(dt):
1597-
return False
1598-
return dt.month == self.month and dt.day == self._get_offset_day(dt)
1599-
1600-
def __init__(self, n=1, normalize=False, month=None):
1601-
BaseOffset.__init__(self, n, normalize)
1602-
1603-
month = month if month is not None else self._default_month
1604-
object.__setattr__(self, "month", month)
1605-
1606-
if self.month < 1 or self.month > 12:
1607-
raise ValueError("Month must go from 1 to 12")
1608-
1609-
@classmethod
1610-
def _from_name(cls, suffix=None):
1611-
kwargs = {}
1612-
if suffix:
1613-
kwargs["month"] = ccalendar.MONTH_TO_CAL_NUM[suffix]
1614-
return cls(**kwargs)
1615-
1616-
@property
1617-
def rule_code(self) -> str:
1618-
month = ccalendar.MONTH_ALIASES[self.month]
1619-
return f"{self._prefix}-{month}"
1620-
1621-
1622-
class BYearEnd(YearOffset):
1568+
class BYearEnd(liboffsets.YearOffset):
16231569
"""
16241570
DateOffset increments between business EOM dates.
16251571
"""
@@ -1630,7 +1576,7 @@ class BYearEnd(YearOffset):
16301576
_day_opt = "business_end"
16311577

16321578

1633-
class BYearBegin(YearOffset):
1579+
class BYearBegin(liboffsets.YearOffset):
16341580
"""
16351581
DateOffset increments between business year begin dates.
16361582
"""
@@ -1641,7 +1587,7 @@ class BYearBegin(YearOffset):
16411587
_day_opt = "business_start"
16421588

16431589

1644-
class YearEnd(YearOffset):
1590+
class YearEnd(liboffsets.YearOffset):
16451591
"""
16461592
DateOffset increments between calendar year ends.
16471593
"""
@@ -1651,7 +1597,7 @@ class YearEnd(YearOffset):
16511597
_day_opt = "end"
16521598

16531599

1654-
class YearBegin(YearOffset):
1600+
class YearBegin(liboffsets.YearOffset):
16551601
"""
16561602
DateOffset increments between calendar year begin dates.
16571603
"""

0 commit comments

Comments
 (0)