Skip to content

Commit 25a2fab

Browse files
authored
REF: Make QuarterOffset a cdef class (#34282)
1 parent a9ad632 commit 25a2fab

File tree

4 files changed

+92
-104
lines changed

4 files changed

+92
-104
lines changed

doc/source/reference/offset_frequency.rst

+4-34
Original file line numberDiff line numberDiff line change
@@ -613,40 +613,6 @@ Methods
613613
LastWeekOfMonth.is_on_offset
614614
LastWeekOfMonth.__call__
615615

616-
QuarterOffset
617-
-------------
618-
.. autosummary::
619-
:toctree: api/
620-
621-
QuarterOffset
622-
623-
Properties
624-
~~~~~~~~~~
625-
.. autosummary::
626-
:toctree: api/
627-
628-
QuarterOffset.freqstr
629-
QuarterOffset.kwds
630-
QuarterOffset.name
631-
QuarterOffset.nanos
632-
QuarterOffset.normalize
633-
QuarterOffset.rule_code
634-
QuarterOffset.n
635-
636-
Methods
637-
~~~~~~~
638-
.. autosummary::
639-
:toctree: api/
640-
641-
QuarterOffset.apply
642-
QuarterOffset.apply_index
643-
QuarterOffset.copy
644-
QuarterOffset.isAnchored
645-
QuarterOffset.onOffset
646-
QuarterOffset.is_anchored
647-
QuarterOffset.is_on_offset
648-
QuarterOffset.__call__
649-
650616
BQuarterEnd
651617
-----------
652618
.. autosummary::
@@ -666,6 +632,7 @@ Properties
666632
BQuarterEnd.normalize
667633
BQuarterEnd.rule_code
668634
BQuarterEnd.n
635+
BQuarterEnd.startingMonth
669636

670637
Methods
671638
~~~~~~~
@@ -700,6 +667,7 @@ Properties
700667
BQuarterBegin.normalize
701668
BQuarterBegin.rule_code
702669
BQuarterBegin.n
670+
BQuarterBegin.startingMonth
703671

704672
Methods
705673
~~~~~~~
@@ -734,6 +702,7 @@ Properties
734702
QuarterEnd.normalize
735703
QuarterEnd.rule_code
736704
QuarterEnd.n
705+
QuarterEnd.startingMonth
737706

738707
Methods
739708
~~~~~~~
@@ -768,6 +737,7 @@ Properties
768737
QuarterBegin.normalize
769738
QuarterBegin.rule_code
770739
QuarterBegin.n
740+
QuarterBegin.startingMonth
771741

772742
Methods
773743
~~~~~~~

pandas/_libs/tslibs/offsets.pyx

+78-2
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,13 @@ class BusinessHourMixin(BusinessMixin):
10601060
object.__setattr__(self, "start", start)
10611061
object.__setattr__(self, "end", end)
10621062

1063+
@classmethod
1064+
def _from_name(cls, suffix=None):
1065+
# default _from_name calls cls with no args
1066+
if suffix:
1067+
raise ValueError(f"Bad freq suffix {suffix}")
1068+
return cls()
1069+
10631070
def _repr_attrs(self) -> str:
10641071
out = super()._repr_attrs()
10651072
hours = ",".join(
@@ -1223,6 +1230,75 @@ cdef class YearOffset(BaseOffset):
12231230
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)
12241231

12251232

1233+
cdef class QuarterOffset(BaseOffset):
1234+
_attributes = frozenset(["n", "normalize", "startingMonth"])
1235+
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
1236+
# point. Also apply_index, is_on_offset, rule_code if
1237+
# startingMonth vs month attr names are resolved
1238+
1239+
# FIXME: python annotations here breaks things
1240+
# _default_startingMonth: int
1241+
# _from_name_startingMonth: int
1242+
1243+
cdef readonly:
1244+
int startingMonth
1245+
1246+
def __init__(self, n=1, normalize=False, startingMonth=None):
1247+
BaseOffset.__init__(self, n, normalize)
1248+
1249+
if startingMonth is None:
1250+
startingMonth = self._default_startingMonth
1251+
self.startingMonth = startingMonth
1252+
1253+
def __reduce__(self):
1254+
return type(self), (self.n, self.normalize, self.startingMonth)
1255+
1256+
@classmethod
1257+
def _from_name(cls, suffix=None):
1258+
kwargs = {}
1259+
if suffix:
1260+
kwargs["startingMonth"] = MONTH_TO_CAL_NUM[suffix]
1261+
else:
1262+
if cls._from_name_startingMonth is not None:
1263+
kwargs["startingMonth"] = cls._from_name_startingMonth
1264+
return cls(**kwargs)
1265+
1266+
@property
1267+
def rule_code(self) -> str:
1268+
month = MONTH_ALIASES[self.startingMonth]
1269+
return f"{self._prefix}-{month}"
1270+
1271+
def is_anchored(self) -> bool:
1272+
return self.n == 1 and self.startingMonth is not None
1273+
1274+
def is_on_offset(self, dt) -> bool:
1275+
if self.normalize and not is_normalized(dt):
1276+
return False
1277+
mod_month = (dt.month - self.startingMonth) % 3
1278+
return mod_month == 0 and dt.day == self._get_offset_day(dt)
1279+
1280+
@apply_wraps
1281+
def apply(self, other):
1282+
# months_since: find the calendar quarter containing other.month,
1283+
# e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
1284+
# Then find the month in that quarter containing an is_on_offset date for
1285+
# self. `months_since` is the number of months to shift other.month
1286+
# to get to this on-offset month.
1287+
months_since = other.month % 3 - self.startingMonth % 3
1288+
qtrs = roll_qtrday(
1289+
other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3
1290+
)
1291+
months = qtrs * 3 - months_since
1292+
return shift_month(other, months, self._day_opt)
1293+
1294+
@apply_index_wraps
1295+
def apply_index(self, dtindex):
1296+
shifted = shift_quarters(
1297+
dtindex.asi8, self.n, self.startingMonth, self._day_opt
1298+
)
1299+
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)
1300+
1301+
12261302
# ----------------------------------------------------------------------
12271303
# RelativeDelta Arithmetic
12281304

@@ -1268,7 +1344,7 @@ cdef inline int month_add_months(npy_datetimestruct dts, int months) nogil:
12681344

12691345
@cython.wraparound(False)
12701346
@cython.boundscheck(False)
1271-
def shift_quarters(
1347+
cdef shift_quarters(
12721348
const int64_t[:] dtindex,
12731349
int quarters,
12741350
int q1start_month,
@@ -1612,7 +1688,7 @@ def shift_month(stamp: datetime, months: int,
16121688
return stamp.replace(year=year, month=month, day=day)
16131689

16141690

1615-
cpdef int get_day_of_month(datetime other, day_opt) except? -1:
1691+
cdef int get_day_of_month(datetime other, day_opt) except? -1:
16161692
"""
16171693
Find the day in `other`'s month that satisfies a DateOffset's is_on_offset
16181694
policy, as described by the `day_opt` argument.

pandas/tests/tseries/offsets/test_offsets_properties.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
# enough runtime information (e.g. type hints) to infer how to build them.
6363
gen_yqm_offset = st.one_of(
6464
*map(
65-
st.from_type,
65+
st.from_type, # type: ignore
6666
[
6767
MonthBegin,
6868
MonthEnd,

pandas/tseries/offsets.py

+9-67
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
apply_wraps,
3131
as_datetime,
3232
is_normalized,
33-
roll_yearday,
3433
shift_month,
3534
to_dt64D,
3635
)
@@ -311,13 +310,6 @@ def freqstr(self):
311310

312311

313312
class SingleConstructorMixin:
314-
@classmethod
315-
def _from_name(cls, suffix=None):
316-
# default _from_name calls cls with no args
317-
if suffix:
318-
raise ValueError(f"Bad freq suffix {suffix}")
319-
return cls()
320-
321313
@cache_readonly
322314
def _params(self):
323315
# TODO: see if we can just write cache_readonly(BaseOffset._params.__get__)
@@ -330,7 +322,12 @@ def freqstr(self):
330322

331323

332324
class SingleConstructorOffset(SingleConstructorMixin, BaseOffset):
333-
pass
325+
@classmethod
326+
def _from_name(cls, suffix=None):
327+
# default _from_name calls cls with no args
328+
if suffix:
329+
raise ValueError(f"Bad freq suffix {suffix}")
330+
return cls()
334331

335332

336333
class BusinessDay(BusinessMixin, SingleConstructorOffset):
@@ -1446,69 +1443,13 @@ def _from_name(cls, suffix=None):
14461443
# Quarter-Based Offset Classes
14471444

14481445

1449-
class QuarterOffset(SingleConstructorOffset):
1446+
class QuarterOffset(SingleConstructorMixin, liboffsets.QuarterOffset):
14501447
"""
1451-
Quarter representation - doesn't call super.
1448+
Quarter representation.
14521449
"""
14531450

14541451
_default_startingMonth: Optional[int] = None
14551452
_from_name_startingMonth: Optional[int] = None
1456-
_attributes = frozenset(["n", "normalize", "startingMonth"])
1457-
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
1458-
# point. Also apply_index, is_on_offset, rule_code if
1459-
# startingMonth vs month attr names are resolved
1460-
1461-
def __init__(self, n=1, normalize=False, startingMonth=None):
1462-
BaseOffset.__init__(self, n, normalize)
1463-
1464-
if startingMonth is None:
1465-
startingMonth = self._default_startingMonth
1466-
object.__setattr__(self, "startingMonth", startingMonth)
1467-
1468-
def is_anchored(self) -> bool:
1469-
return self.n == 1 and self.startingMonth is not None
1470-
1471-
@classmethod
1472-
def _from_name(cls, suffix=None):
1473-
kwargs = {}
1474-
if suffix:
1475-
kwargs["startingMonth"] = ccalendar.MONTH_TO_CAL_NUM[suffix]
1476-
else:
1477-
if cls._from_name_startingMonth is not None:
1478-
kwargs["startingMonth"] = cls._from_name_startingMonth
1479-
return cls(**kwargs)
1480-
1481-
@property
1482-
def rule_code(self) -> str:
1483-
month = ccalendar.MONTH_ALIASES[self.startingMonth]
1484-
return f"{self._prefix}-{month}"
1485-
1486-
@apply_wraps
1487-
def apply(self, other):
1488-
# months_since: find the calendar quarter containing other.month,
1489-
# e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
1490-
# Then find the month in that quarter containing an is_on_offset date for
1491-
# self. `months_since` is the number of months to shift other.month
1492-
# to get to this on-offset month.
1493-
months_since = other.month % 3 - self.startingMonth % 3
1494-
qtrs = liboffsets.roll_qtrday(
1495-
other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3
1496-
)
1497-
months = qtrs * 3 - months_since
1498-
return shift_month(other, months, self._day_opt)
1499-
1500-
def is_on_offset(self, dt: datetime) -> bool:
1501-
if self.normalize and not is_normalized(dt):
1502-
return False
1503-
mod_month = (dt.month - self.startingMonth) % 3
1504-
return mod_month == 0 and dt.day == self._get_offset_day(dt)
1505-
1506-
@apply_index_wraps
1507-
def apply_index(self, dtindex):
1508-
shifted = liboffsets.shift_quarters(
1509-
dtindex.asi8, self.n, self.startingMonth, self._day_opt
1510-
)
1511-
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)
15121453

15131454

15141455
class BQuarterEnd(QuarterOffset):
@@ -1531,6 +1472,7 @@ class BQuarterEnd(QuarterOffset):
15311472
class BQuarterBegin(QuarterOffset):
15321473
_outputName = "BusinessQuarterBegin"
15331474
# I suspect this is wrong for *all* of them.
1475+
# TODO: What does the above comment refer to?
15341476
_default_startingMonth = 3
15351477
_from_name_startingMonth = 1
15361478
_prefix = "BQS"

0 commit comments

Comments
 (0)