Skip to content

REF: Make QuarterOffset a cdef class #34282

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 4 commits into from
May 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 4 additions & 34 deletions doc/source/reference/offset_frequency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -613,40 +613,6 @@ Methods
LastWeekOfMonth.is_on_offset
LastWeekOfMonth.__call__

QuarterOffset
-------------
.. autosummary::
:toctree: api/

QuarterOffset

Properties
~~~~~~~~~~
.. autosummary::
:toctree: api/

QuarterOffset.freqstr
QuarterOffset.kwds
QuarterOffset.name
QuarterOffset.nanos
QuarterOffset.normalize
QuarterOffset.rule_code
QuarterOffset.n

Methods
~~~~~~~
.. autosummary::
:toctree: api/

QuarterOffset.apply
QuarterOffset.apply_index
QuarterOffset.copy
QuarterOffset.isAnchored
QuarterOffset.onOffset
QuarterOffset.is_anchored
QuarterOffset.is_on_offset
QuarterOffset.__call__

BQuarterEnd
-----------
.. autosummary::
Expand All @@ -666,6 +632,7 @@ Properties
BQuarterEnd.normalize
BQuarterEnd.rule_code
BQuarterEnd.n
BQuarterEnd.startingMonth

Methods
~~~~~~~
Expand Down Expand Up @@ -700,6 +667,7 @@ Properties
BQuarterBegin.normalize
BQuarterBegin.rule_code
BQuarterBegin.n
BQuarterBegin.startingMonth

Methods
~~~~~~~
Expand Down Expand Up @@ -734,6 +702,7 @@ Properties
QuarterEnd.normalize
QuarterEnd.rule_code
QuarterEnd.n
QuarterEnd.startingMonth

Methods
~~~~~~~
Expand Down Expand Up @@ -768,6 +737,7 @@ Properties
QuarterBegin.normalize
QuarterBegin.rule_code
QuarterBegin.n
QuarterBegin.startingMonth

Methods
~~~~~~~
Expand Down
80 changes: 78 additions & 2 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,13 @@ class BusinessHourMixin(BusinessMixin):
object.__setattr__(self, "start", start)
object.__setattr__(self, "end", end)

@classmethod
def _from_name(cls, suffix=None):
# default _from_name calls cls with no args
if suffix:
raise ValueError(f"Bad freq suffix {suffix}")
return cls()

def _repr_attrs(self) -> str:
out = super()._repr_attrs()
hours = ",".join(
Expand Down Expand Up @@ -1223,6 +1230,75 @@ cdef class YearOffset(BaseOffset):
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


cdef class QuarterOffset(BaseOffset):
_attributes = frozenset(["n", "normalize", "startingMonth"])
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
# point. Also apply_index, is_on_offset, rule_code if
# startingMonth vs month attr names are resolved

# FIXME: python annotations here breaks things
# _default_startingMonth: int
# _from_name_startingMonth: int

cdef readonly:
int startingMonth

def __init__(self, n=1, normalize=False, startingMonth=None):
BaseOffset.__init__(self, n, normalize)

if startingMonth is None:
startingMonth = self._default_startingMonth
self.startingMonth = startingMonth

def __reduce__(self):
return type(self), (self.n, self.normalize, self.startingMonth)

@classmethod
def _from_name(cls, suffix=None):
kwargs = {}
if suffix:
kwargs["startingMonth"] = MONTH_TO_CAL_NUM[suffix]
else:
if cls._from_name_startingMonth is not None:
kwargs["startingMonth"] = cls._from_name_startingMonth
return cls(**kwargs)

@property
def rule_code(self) -> str:
month = MONTH_ALIASES[self.startingMonth]
return f"{self._prefix}-{month}"

def is_anchored(self) -> bool:
return self.n == 1 and self.startingMonth is not None

def is_on_offset(self, dt) -> bool:
if self.normalize and not is_normalized(dt):
return False
mod_month = (dt.month - self.startingMonth) % 3
return mod_month == 0 and dt.day == self._get_offset_day(dt)

@apply_wraps
def apply(self, other):
# months_since: find the calendar quarter containing other.month,
# e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
# Then find the month in that quarter containing an is_on_offset date for
# self. `months_since` is the number of months to shift other.month
# to get to this on-offset month.
months_since = other.month % 3 - self.startingMonth % 3
qtrs = roll_qtrday(
other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3
)
months = qtrs * 3 - months_since
return shift_month(other, months, self._day_opt)

@apply_index_wraps
def apply_index(self, dtindex):
shifted = shift_quarters(
dtindex.asi8, self.n, self.startingMonth, self._day_opt
)
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


# ----------------------------------------------------------------------
# RelativeDelta Arithmetic

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

@cython.wraparound(False)
@cython.boundscheck(False)
def shift_quarters(
cdef shift_quarters(
const int64_t[:] dtindex,
int quarters,
int q1start_month,
Expand Down Expand Up @@ -1612,7 +1688,7 @@ def shift_month(stamp: datetime, months: int,
return stamp.replace(year=year, month=month, day=day)


cpdef int get_day_of_month(datetime other, day_opt) except? -1:
cdef int get_day_of_month(datetime other, day_opt) except? -1:
"""
Find the day in `other`'s month that satisfies a DateOffset's is_on_offset
policy, as described by the `day_opt` argument.
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/tseries/offsets/test_offsets_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
# enough runtime information (e.g. type hints) to infer how to build them.
gen_yqm_offset = st.one_of(
*map(
st.from_type,
st.from_type, # type: ignore
[
MonthBegin,
MonthEnd,
Expand Down
76 changes: 9 additions & 67 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
apply_wraps,
as_datetime,
is_normalized,
roll_yearday,
shift_month,
to_dt64D,
)
Expand Down Expand Up @@ -311,13 +310,6 @@ def freqstr(self):


class SingleConstructorMixin:
@classmethod
def _from_name(cls, suffix=None):
# default _from_name calls cls with no args
if suffix:
raise ValueError(f"Bad freq suffix {suffix}")
return cls()

@cache_readonly
def _params(self):
# TODO: see if we can just write cache_readonly(BaseOffset._params.__get__)
Expand All @@ -330,7 +322,12 @@ def freqstr(self):


class SingleConstructorOffset(SingleConstructorMixin, BaseOffset):
pass
@classmethod
def _from_name(cls, suffix=None):
# default _from_name calls cls with no args
if suffix:
raise ValueError(f"Bad freq suffix {suffix}")
return cls()


class BusinessDay(BusinessMixin, SingleConstructorOffset):
Expand Down Expand Up @@ -1447,69 +1444,13 @@ def _from_name(cls, suffix=None):
# Quarter-Based Offset Classes


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

_default_startingMonth: Optional[int] = None
_from_name_startingMonth: Optional[int] = None
_attributes = frozenset(["n", "normalize", "startingMonth"])
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
# point. Also apply_index, is_on_offset, rule_code if
# startingMonth vs month attr names are resolved

def __init__(self, n=1, normalize=False, startingMonth=None):
BaseOffset.__init__(self, n, normalize)

if startingMonth is None:
startingMonth = self._default_startingMonth
object.__setattr__(self, "startingMonth", startingMonth)

def is_anchored(self) -> bool:
return self.n == 1 and self.startingMonth is not None

@classmethod
def _from_name(cls, suffix=None):
kwargs = {}
if suffix:
kwargs["startingMonth"] = ccalendar.MONTH_TO_CAL_NUM[suffix]
else:
if cls._from_name_startingMonth is not None:
kwargs["startingMonth"] = cls._from_name_startingMonth
return cls(**kwargs)

@property
def rule_code(self) -> str:
month = ccalendar.MONTH_ALIASES[self.startingMonth]
return f"{self._prefix}-{month}"

@apply_wraps
def apply(self, other):
# months_since: find the calendar quarter containing other.month,
# e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
# Then find the month in that quarter containing an is_on_offset date for
# self. `months_since` is the number of months to shift other.month
# to get to this on-offset month.
months_since = other.month % 3 - self.startingMonth % 3
qtrs = liboffsets.roll_qtrday(
other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3
)
months = qtrs * 3 - months_since
return shift_month(other, months, self._day_opt)

def is_on_offset(self, dt: datetime) -> bool:
if self.normalize and not is_normalized(dt):
return False
mod_month = (dt.month - self.startingMonth) % 3
return mod_month == 0 and dt.day == self._get_offset_day(dt)

@apply_index_wraps
def apply_index(self, dtindex):
shifted = liboffsets.shift_quarters(
dtindex.asi8, self.n, self.startingMonth, self._day_opt
)
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


class BQuarterEnd(QuarterOffset):
Expand All @@ -1532,6 +1473,7 @@ class BQuarterEnd(QuarterOffset):
class BQuarterBegin(QuarterOffset):
_outputName = "BusinessQuarterBegin"
# I suspect this is wrong for *all* of them.
# TODO: What does the above comment refer to?
_default_startingMonth = 3
_from_name_startingMonth = 1
_prefix = "BQS"
Expand Down