Skip to content

Commit 7fa15cd

Browse files
committed
ENH: Add HalfYear offsets
1 parent 6bcd303 commit 7fa15cd

File tree

6 files changed

+942
-2
lines changed

6 files changed

+942
-2
lines changed

pandas/_libs/tslibs/offsets.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ class BQuarterEnd(QuarterOffset): ...
168168
class BQuarterBegin(QuarterOffset): ...
169169
class QuarterEnd(QuarterOffset): ...
170170
class QuarterBegin(QuarterOffset): ...
171+
172+
class HalfYearOffset(SingleConstructorOffset):
173+
def __init__(
174+
self, n: int = ..., normalize: bool = ..., startingMonth: int | None = ...
175+
) -> None: ...
176+
177+
class BHalfYearEnd(HalfYearOffset): ...
178+
class BHalfYearBegin(HalfYearOffset): ...
179+
class HalfYearEnd(HalfYearOffset): ...
180+
class HalfYearBegin(HalfYearOffset): ...
171181
class MonthOffset(SingleConstructorOffset): ...
172182
class MonthEnd(MonthOffset): ...
173183
class MonthBegin(MonthOffset): ...

pandas/_libs/tslibs/offsets.pyx

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,6 +3011,228 @@ cdef class QuarterBegin(QuarterOffset):
30113011
_day_opt = "start"
30123012

30133013

3014+
# ----------------------------------------------------------------------
3015+
# HalfYear-Based Offset Classes
3016+
3017+
cdef class HalfYearOffset(SingleConstructorOffset):
3018+
_attributes = tuple(["n", "normalize", "startingMonth"])
3019+
# TODO: Consider combining HalfYearOffset, QuarterOffset and YearOffset
3020+
3021+
# FIXME(cython#4446): python annotation here gives compile-time errors
3022+
# _default_starting_month: int
3023+
# _from_name_starting_month: int
3024+
3025+
cdef readonly:
3026+
int startingMonth
3027+
3028+
def __init__(self, n=1, normalize=False, startingMonth=None):
3029+
BaseOffset.__init__(self, n, normalize)
3030+
3031+
if startingMonth is None:
3032+
startingMonth = self._default_starting_month
3033+
self.startingMonth = startingMonth
3034+
3035+
cpdef __setstate__(self, state):
3036+
self.startingMonth = state.pop("startingMonth")
3037+
self.n = state.pop("n")
3038+
self.normalize = state.pop("normalize")
3039+
3040+
@classmethod
3041+
def _from_name(cls, suffix=None):
3042+
kwargs = {}
3043+
if suffix:
3044+
kwargs["startingMonth"] = MONTH_TO_CAL_NUM[suffix]
3045+
else:
3046+
if cls._from_name_starting_month is not None:
3047+
kwargs["startingMonth"] = cls._from_name_starting_month
3048+
return cls(**kwargs)
3049+
3050+
@property
3051+
def rule_code(self) -> str:
3052+
month = MONTH_ALIASES[self.startingMonth]
3053+
return f"{self._prefix}-{month}"
3054+
3055+
def is_on_offset(self, dt: datetime) -> bool:
3056+
if self.normalize and not _is_normalized(dt):
3057+
return False
3058+
mod_month = (dt.month - self.startingMonth) % 6
3059+
return mod_month == 0 and dt.day == self._get_offset_day(dt)
3060+
3061+
@apply_wraps
3062+
def _apply(self, other: datetime) -> datetime:
3063+
# months_since: find the calendar half containing other.month,
3064+
# e.g. if other.month == 8, the calendar half is [Jul, Aug, Sep, ..., Dec].
3065+
# Then find the month in that half containing an is_on_offset date for
3066+
# self. `months_since` is the number of months to shift other.month
3067+
# to get to this on-offset month.
3068+
months_since = other.month % 6 - self.startingMonth % 6
3069+
hlvs = roll_qtrday(
3070+
other, self.n, self.startingMonth, day_opt=self._day_opt, modby=6
3071+
)
3072+
months = hlvs * 6 - months_since
3073+
return shift_month(other, months, self._day_opt)
3074+
3075+
def _apply_array(self, dtarr: np.ndarray) -> np.ndarray:
3076+
reso = get_unit_from_dtype(dtarr.dtype)
3077+
shifted = shift_quarters(
3078+
dtarr.view("i8"),
3079+
self.n,
3080+
self.startingMonth,
3081+
self._day_opt,
3082+
modby=6,
3083+
reso=reso,
3084+
)
3085+
return shifted
3086+
3087+
3088+
cdef class BHalfYearEnd(HalfYearOffset):
3089+
"""
3090+
DateOffset increments between the last business day of each half-year.
3091+
3092+
startingMonth = 1 corresponds to dates like 1/31/2007, 7/31/2007, ...
3093+
startingMonth = 2 corresponds to dates like 2/28/2007, 8/31/2007, ...
3094+
startingMonth = 6 corresponds to dates like 6/30/2007, 12/31/2007, ...
3095+
3096+
Attributes
3097+
----------
3098+
n : int, default 1
3099+
The number of half-years represented.
3100+
normalize : bool, default False
3101+
Normalize start/end dates to midnight before generating date range.
3102+
startingMonth : int, default 6
3103+
A specific integer for the month of the year from which we start half-years.
3104+
3105+
See Also
3106+
--------
3107+
:class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
3108+
3109+
Examples
3110+
--------
3111+
>>> from pandas.tseries.offsets import BHalfYearEnd
3112+
>>> ts = pd.Timestamp('2020-05-24 05:01:15')
3113+
>>> ts + BHalfYearEnd()
3114+
Timestamp('2020-06-30 05:01:15')
3115+
>>> ts + BHalfYearEnd(2)
3116+
Timestamp('2020-12-31 05:01:15')
3117+
>>> ts + BHalfYearEnd(1, startingMonth=2)
3118+
Timestamp('2020-08-31 05:01:15')
3119+
>>> ts + BHalfYearEnd(startingMonth=2)
3120+
Timestamp('2020-08-31 05:01:15')
3121+
"""
3122+
_output_name = "BusinessHalfYearEnd"
3123+
_default_starting_month = 6
3124+
_from_name_starting_month = 12
3125+
_prefix = "BHYE"
3126+
_day_opt = "business_end"
3127+
3128+
3129+
cdef class BHalfYearBegin(HalfYearOffset):
3130+
"""
3131+
DateOffset increments between the first business day of each half-year.
3132+
3133+
startingMonth = 1 corresponds to dates like 1/01/2007, 7/01/2007, ...
3134+
startingMonth = 2 corresponds to dates like 2/01/2007, 8/01/2007, ...
3135+
startingMonth = 3 corresponds to dates like 3/01/2007, 9/01/2007, ...
3136+
3137+
Attributes
3138+
----------
3139+
n : int, default 1
3140+
The number of half-years represented.
3141+
normalize : bool, default False
3142+
Normalize start/end dates to midnight before generating date range.
3143+
startingMonth : int, default 1
3144+
A specific integer for the month of the year from which we start half-years.
3145+
3146+
See Also
3147+
--------
3148+
:class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
3149+
3150+
Examples
3151+
--------
3152+
>>> from pandas.tseries.offsets import BHalfYearBegin
3153+
>>> ts = pd.Timestamp('2020-05-24 05:01:15')
3154+
>>> ts + BHalfYearBegin()
3155+
Timestamp('2020-07-01 05:01:15')
3156+
>>> ts + BHalfYearBegin(2)
3157+
Timestamp('2021-01-01 05:01:15')
3158+
>>> ts + BHalfYearBegin(startingMonth=2)
3159+
Timestamp('2020-08-03 05:01:15')
3160+
>>> ts + BHalfYearBegin(-1)
3161+
Timestamp('2020-01-01 05:01:15')
3162+
"""
3163+
_output_name = "BusinessHalfYearBegin"
3164+
_default_starting_month = 1
3165+
_from_name_starting_month = 1
3166+
_prefix = "BHYS"
3167+
_day_opt = "business_start"
3168+
3169+
3170+
cdef class HalfYearEnd(HalfYearOffset):
3171+
"""
3172+
DateOffset increments between half-year end dates.
3173+
3174+
startingMonth = 1 corresponds to dates like 1/31/2007, 7/31/2007, ...
3175+
startingMonth = 2 corresponds to dates like 2/28/2007, 8/31/2007, ...
3176+
startingMonth = 6 corresponds to dates like 6/30/2007, 12/31/2007, ...
3177+
3178+
Attributes
3179+
----------
3180+
n : int, default 1
3181+
The number of half-years represented.
3182+
normalize : bool, default False
3183+
Normalize start/end dates to midnight before generating date range.
3184+
startingMonth : int, default 6
3185+
A specific integer for the month of the year from which we start half-years.
3186+
3187+
See Also
3188+
--------
3189+
:class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
3190+
3191+
Examples
3192+
--------
3193+
>>> ts = pd.Timestamp(2022, 1, 1)
3194+
>>> ts + pd.offsets.HalfYearEnd()
3195+
Timestamp('2022-06-30 00:00:00')
3196+
"""
3197+
_default_starting_month = 6
3198+
_from_name_starting_month = 12
3199+
_prefix = "HYE"
3200+
_day_opt = "end"
3201+
3202+
3203+
cdef class HalfYearBegin(HalfYearOffset):
3204+
"""
3205+
DateOffset increments between half-year start dates.
3206+
3207+
startingMonth = 1 corresponds to dates like 1/01/2007, 7/01/2007, ...
3208+
startingMonth = 2 corresponds to dates like 2/01/2007, 8/01/2007, ...
3209+
startingMonth = 3 corresponds to dates like 3/01/2007, 9/01/2007, ...
3210+
3211+
Attributes
3212+
----------
3213+
n : int, default 1
3214+
The number of half-years represented.
3215+
normalize : bool, default False
3216+
Normalize start/end dates to midnight before generating date range.
3217+
startingMonth : int, default 1
3218+
A specific integer for the month of the year from which we start half-years.
3219+
3220+
See Also
3221+
--------
3222+
:class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
3223+
3224+
Examples
3225+
--------
3226+
>>> ts = pd.Timestamp(2022, 2, 1)
3227+
>>> ts + pd.offsets.HalfYearBegin()
3228+
Timestamp('2022-07-01 00:00:00')
3229+
"""
3230+
_default_starting_month = 1
3231+
_from_name_starting_month = 1
3232+
_prefix = "HYS"
3233+
_day_opt = "start"
3234+
3235+
30143236
# ----------------------------------------------------------------------
30153237
# Month-Based Offset Classes
30163238

@@ -4823,6 +5045,8 @@ prefix_mapping = {
48235045
BusinessMonthEnd, # 'BME'
48245046
BQuarterEnd, # 'BQE'
48255047
BQuarterBegin, # 'BQS'
5048+
BHalfYearEnd, # 'BHYE'
5049+
BHalfYearBegin, # 'BHYS'
48265050
BusinessHour, # 'bh'
48275051
CustomBusinessDay, # 'C'
48285052
CustomBusinessMonthEnd, # 'CBME'
@@ -4839,6 +5063,8 @@ prefix_mapping = {
48395063
Micro, # 'us'
48405064
QuarterEnd, # 'QE'
48415065
QuarterBegin, # 'QS'
5066+
HalfYearEnd, # 'HYE'
5067+
HalfYearBegin, # 'HYS'
48425068
Milli, # 'ms'
48435069
Hour, # 'h'
48445070
Day, # 'D'

0 commit comments

Comments
 (0)