Skip to content

REF: move Year/Month/Quarter offsets to liboffsets #34346

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 2 commits into from
May 25, 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
167 changes: 163 additions & 4 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,12 @@ cdef class BaseOffset:
equality between two DateOffset objects.
"""
# NB: non-cython subclasses override property with cache_readonly
all_paras = self.__dict__.copy()
d = getattr(self, "__dict__", {})
all_paras = d.copy()
all_paras["n"] = self.n
all_paras["normalize"] = self.normalize
for attr in self._attributes:
if hasattr(self, attr) and attr not in self.__dict__:
if hasattr(self, attr) and attr not in d:
# cython attributes are not in __dict__
all_paras[attr] = getattr(self, attr)

Expand Down Expand Up @@ -711,6 +712,15 @@ cdef class BaseOffset:

def __setstate__(self, state):
"""Reconstruct an instance from a pickled state"""
if isinstance(self, MonthOffset):
# We can't just override MonthOffset.__setstate__ because of the
# combination of MRO resolution and cython not handling
# multiple inheritance nicely for cdef classes.
state.pop("_use_relativedelta", False)
state.pop("offset", None)
state.pop("_offset", None)
state.pop("kwds", {})

if 'offset' in state:
# Older (<0.22.0) versions have offset attribute instead of _offset
if '_offset' in state: # pragma: no cover
Expand All @@ -728,6 +738,12 @@ cdef class BaseOffset:
self.n = state.pop("n")
self.normalize = state.pop("normalize")
self._cache = state.pop("_cache", {})

if not len(state):
# FIXME: kludge because some classes no longer have a __dict__,
# so we need to short-circuit before raising on the next line
return

self.__dict__.update(state)

if 'weekmask' in state and 'holidays' in state:
Expand All @@ -739,7 +755,7 @@ cdef class BaseOffset:

def __getstate__(self):
"""Return a pickleable state"""
state = self.__dict__.copy()
state = getattr(self, "__dict__", {}).copy()
state["n"] = self.n
state["normalize"] = self.normalize

Expand Down Expand Up @@ -1020,8 +1036,11 @@ cdef class BusinessMixin(SingleConstructorOffset):

cpdef __setstate__(self, state):
# We need to use a cdef/cpdef method to set the readonly _offset attribute
if "_offset" in state:
self._offset = state.pop("_offset")
elif "offset" in state:
self._offset = state.pop("offset")
BaseOffset.__setstate__(self, state)
self._offset = state["_offset"]


class BusinessHourMixin(BusinessMixin):
Expand Down Expand Up @@ -1180,6 +1199,7 @@ class WeekOfMonthMixin(SingleConstructorOffset):


# ----------------------------------------------------------------------
# Year-Based Offset Classes

cdef class YearOffset(SingleConstructorOffset):
"""
Expand All @@ -1201,6 +1221,12 @@ cdef class YearOffset(SingleConstructorOffset):
if month < 1 or month > 12:
raise ValueError("Month must go from 1 to 12")

cpdef __setstate__(self, state):
self.month = state.pop("month")
self.n = state.pop("n")
self.normalize = state.pop("normalize")
self._cache = {}

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

Expand Down Expand Up @@ -1242,6 +1268,51 @@ cdef class YearOffset(SingleConstructorOffset):
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


cdef class BYearEnd(YearOffset):
"""
DateOffset increments between business EOM dates.
"""

_outputName = "BusinessYearEnd"
_default_month = 12
_prefix = "BA"
_day_opt = "business_end"


cdef class BYearBegin(YearOffset):
"""
DateOffset increments between business year begin dates.
"""

_outputName = "BusinessYearBegin"
_default_month = 1
_prefix = "BAS"
_day_opt = "business_start"


cdef class YearEnd(YearOffset):
"""
DateOffset increments between calendar year ends.
"""

_default_month = 12
_prefix = "A"
_day_opt = "end"


cdef class YearBegin(YearOffset):
"""
DateOffset increments between calendar year begin dates.
"""

_default_month = 1
_prefix = "AS"
_day_opt = "start"


# ----------------------------------------------------------------------
# Quarter-Based Offset Classes

cdef class QuarterOffset(SingleConstructorOffset):
_attributes = frozenset(["n", "normalize", "startingMonth"])
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
Expand All @@ -1262,6 +1333,11 @@ cdef class QuarterOffset(SingleConstructorOffset):
startingMonth = self._default_startingMonth
self.startingMonth = startingMonth

cpdef __setstate__(self, state):
self.startingMonth = state.pop("startingMonth")
self.n = state.pop("n")
self.normalize = state.pop("normalize")

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

Expand Down Expand Up @@ -1311,6 +1387,57 @@ cdef class QuarterOffset(SingleConstructorOffset):
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


cdef class BQuarterEnd(QuarterOffset):
"""
DateOffset increments between business Quarter dates.

startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ...
"""
_outputName = "BusinessQuarterEnd"
_default_startingMonth = 3
_from_name_startingMonth = 12
_prefix = "BQ"
_day_opt = "business_end"


# TODO: This is basically the same as BQuarterEnd
cdef 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"
_day_opt = "business_start"


cdef class QuarterEnd(QuarterOffset):
"""
DateOffset increments between business Quarter dates.

startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ...
"""
_outputName = "QuarterEnd"
_default_startingMonth = 3
_prefix = "Q"
_day_opt = "end"


cdef class QuarterBegin(QuarterOffset):
_outputName = "QuarterBegin"
_default_startingMonth = 3
_from_name_startingMonth = 1
_prefix = "QS"
_day_opt = "start"


# ----------------------------------------------------------------------
# Month-Based Offset Classes

cdef class MonthOffset(SingleConstructorOffset):
def is_on_offset(self, dt) -> bool:
if self.normalize and not is_normalized(dt):
Expand All @@ -1329,6 +1456,38 @@ cdef class MonthOffset(SingleConstructorOffset):
return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype)


cdef class MonthEnd(MonthOffset):
"""
DateOffset of one month end.
"""
_prefix = "M"
_day_opt = "end"


cdef class MonthBegin(MonthOffset):
"""
DateOffset of one month at beginning.
"""
_prefix = "MS"
_day_opt = "start"


cdef class BusinessMonthEnd(MonthOffset):
"""
DateOffset increments between business EOM dates.
"""
_prefix = "BM"
_day_opt = "business_end"


cdef class BusinessMonthBegin(MonthOffset):
"""
DateOffset of one business month at beginning.
"""
_prefix = "BMS"
_day_opt = "business_start"


# ---------------------------------------------------------------------
# Special Offset Classes

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, # type: ignore
st.from_type,
[
MonthBegin,
MonthEnd,
Expand Down
Loading