Skip to content

Commit 1e151f1

Browse files
committed
BUG: CustomBusinessMonthBegin(End) sometimes ignores extra offset (GH41356)
CustomBusinessMonthBegin does not apply extra offset when initially rolled month begin is already a business day.
1 parent 896256e commit 1e151f1

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed

pandas/_libs/tslibs/offsets.pyx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3350,7 +3350,13 @@ cdef class _CustomBusinessMonth(BusinessMixin):
33503350
"""
33513351
Define default roll function to be called in apply method.
33523352
"""
3353-
cbday = CustomBusinessDay(n=self.n, normalize=False, **self.kwds)
3353+
if self.offset:
3354+
cbday_kwds = self.kwds.copy()
3355+
cbday_kwds['offset'] = timedelta(0)
3356+
else:
3357+
cbday_kwds = self.kwds
3358+
3359+
cbday = CustomBusinessDay(n=1, normalize=False, **cbday_kwds)
33543360

33553361
if self._prefix.endswith("S"):
33563362
# MonthBegin
@@ -3394,6 +3400,9 @@ cdef class _CustomBusinessMonth(BusinessMixin):
33943400

33953401
new = cur_month_offset_date + n * self.m_offset
33963402
result = self.cbday_roll(new)
3403+
3404+
if self.offset:
3405+
result = result + self.offset
33973406
return result
33983407

33993408

pandas/tests/tseries/offsets/test_month.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import (
55
date,
66
datetime,
7+
timedelta,
78
)
89

910
import numpy as np
@@ -204,6 +205,56 @@ def test_datetimeindex(self):
204205
0
205206
] == datetime(2012, 1, 31)
206207

208+
apply_with_extra_offset_cases: _ApplyCases = [
209+
(
210+
CBMonthEnd(n=1, offset=timedelta(days=5)),
211+
{
212+
datetime(2021, 3, 1): datetime(2021, 3, 31) + timedelta(days=5),
213+
datetime(2021, 4, 17): datetime(2021, 4, 30) + timedelta(days=5),
214+
},
215+
),
216+
(
217+
CBMonthEnd(n=2, offset=timedelta(days=40)),
218+
{
219+
datetime(2021, 3, 10): datetime(2021, 4, 30) + timedelta(days=40),
220+
datetime(2021, 4, 30): datetime(2021, 6, 30) + timedelta(days=40),
221+
},
222+
),
223+
(
224+
CBMonthEnd(n=1, offset=timedelta(days=-5)),
225+
{
226+
datetime(2021, 3, 1): datetime(2021, 3, 31) - timedelta(days=5),
227+
datetime(2021, 4, 11): datetime(2021, 4, 30) - timedelta(days=5),
228+
},
229+
),
230+
(
231+
-2 * CBMonthEnd(n=1, offset=timedelta(days=10)),
232+
{
233+
datetime(2021, 3, 1): datetime(2021, 1, 29) + timedelta(days=10),
234+
datetime(2021, 4, 3): datetime(2021, 2, 26) + timedelta(days=10),
235+
},
236+
),
237+
(
238+
CBMonthEnd(n=0, offset=timedelta(days=1)),
239+
{
240+
datetime(2021, 3, 2): datetime(2021, 3, 31) + timedelta(days=1),
241+
datetime(2021, 4, 1): datetime(2021, 4, 30) + timedelta(days=1),
242+
},
243+
),
244+
(
245+
CBMonthEnd(n=1, holidays=["2021-03-31"], offset=timedelta(days=1)),
246+
{
247+
datetime(2021, 3, 2): datetime(2021, 3, 30) + timedelta(days=1),
248+
},
249+
),
250+
]
251+
252+
@pytest.mark.parametrize("case", apply_with_extra_offset_cases)
253+
def test_apply_with_extra_offset(self, case):
254+
offset, cases = case
255+
for base, expected in cases.items():
256+
assert_offset_equal(offset, base, expected)
257+
207258

208259
class TestCustomBusinessMonthBegin(CustomBusinessMonthBase, Base):
209260
_offset = CBMonthBegin
@@ -341,6 +392,58 @@ def test_datetimeindex(self):
341392
0
342393
] == datetime(2012, 1, 3)
343394

395+
apply_with_extra_offset_cases: _ApplyCases = [
396+
(
397+
CBMonthBegin(n=1, offset=timedelta(days=5)),
398+
{
399+
datetime(2021, 3, 1): datetime(2021, 4, 1) + timedelta(days=5),
400+
datetime(2021, 4, 17): datetime(2021, 5, 3) + timedelta(days=5),
401+
},
402+
),
403+
(
404+
CBMonthBegin(n=2, offset=timedelta(days=40)),
405+
{
406+
datetime(2021, 3, 10): datetime(2021, 5, 3) + timedelta(days=40),
407+
datetime(2021, 4, 30): datetime(2021, 6, 1) + timedelta(days=40),
408+
},
409+
),
410+
(
411+
CBMonthBegin(n=1, offset=timedelta(days=-5)),
412+
{
413+
datetime(2021, 3, 1): datetime(2021, 4, 1) - timedelta(days=5),
414+
datetime(2021, 4, 11): datetime(2021, 5, 3) - timedelta(days=5),
415+
},
416+
),
417+
(
418+
-2 * CBMonthBegin(n=1, offset=timedelta(days=10)),
419+
{
420+
datetime(2021, 3, 1): datetime(2021, 1, 1) + timedelta(days=10),
421+
datetime(2021, 4, 3): datetime(2021, 3, 1) + timedelta(days=10),
422+
},
423+
),
424+
(
425+
CBMonthBegin(n=0, offset=timedelta(days=1)),
426+
{
427+
datetime(2021, 3, 2): datetime(2021, 4, 1) + timedelta(days=1),
428+
datetime(2021, 4, 1): datetime(2021, 4, 1) + timedelta(days=1),
429+
},
430+
),
431+
(
432+
CBMonthBegin(
433+
n=1, holidays=["2021-04-01", "2021-04-02"], offset=timedelta(days=1)
434+
),
435+
{
436+
datetime(2021, 3, 2): datetime(2021, 4, 5) + timedelta(days=1),
437+
},
438+
),
439+
]
440+
441+
@pytest.mark.parametrize("case", apply_with_extra_offset_cases)
442+
def test_apply_with_extra_offset(self, case):
443+
offset, cases = case
444+
for base, expected in cases.items():
445+
assert_offset_equal(offset, base, expected)
446+
344447

345448
class TestSemiMonthEnd(Base):
346449
_offset = SemiMonthEnd

0 commit comments

Comments
 (0)