From dcc21434721fb606f7c021ef922dbc0ecdbeb854 Mon Sep 17 00:00:00 2001 From: Jack Liu <5811028+zheyuan-liu@users.noreply.github.com> Date: Fri, 7 May 2021 14:39:57 -0700 Subject: [PATCH 1/2] BUG: CustomBusinessMonthBegin(End) sometimes ignores extra offset (GH41356) CustomBusinessMonthBegin(End) does not apply extra offset when the initially rolled month begin (end) is already a business day. --- pandas/_libs/tslibs/offsets.pyx | 8 +- .../offsets/test_custom_business_month.py | 105 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 6596aebc1892e..0faf5fb0a741a 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3370,7 +3370,10 @@ cdef class _CustomBusinessMonth(BusinessMixin): """ Define default roll function to be called in apply method. """ - cbday = CustomBusinessDay(n=self.n, normalize=False, **self.kwds) + cbday_kwds = self.kwds.copy() + cbday_kwds['offset'] = timedelta(0) + + cbday = CustomBusinessDay(n=1, normalize=False, **cbday_kwds) if self._prefix.endswith("S"): # MonthBegin @@ -3414,6 +3417,9 @@ cdef class _CustomBusinessMonth(BusinessMixin): new = cur_month_offset_date + n * self.m_offset result = self.cbday_roll(new) + + if self.offset: + result = result + self.offset return result diff --git a/pandas/tests/tseries/offsets/test_custom_business_month.py b/pandas/tests/tseries/offsets/test_custom_business_month.py index 4cdd25d6483f7..fb0f331fa3ad3 100644 --- a/pandas/tests/tseries/offsets/test_custom_business_month.py +++ b/pandas/tests/tseries/offsets/test_custom_business_month.py @@ -7,6 +7,7 @@ from datetime import ( date, datetime, + timedelta, ) import numpy as np @@ -200,6 +201,59 @@ def test_datetimeindex(self): 0 ] == datetime(2012, 1, 3) + @pytest.mark.parametrize( + "case", + [ + ( + CBMonthBegin(n=1, offset=timedelta(days=5)), + { + datetime(2021, 3, 1): datetime(2021, 4, 1) + timedelta(days=5), + datetime(2021, 4, 17): datetime(2021, 5, 3) + timedelta(days=5), + }, + ), + ( + CBMonthBegin(n=2, offset=timedelta(days=40)), + { + datetime(2021, 3, 10): datetime(2021, 5, 3) + timedelta(days=40), + datetime(2021, 4, 30): datetime(2021, 6, 1) + timedelta(days=40), + }, + ), + ( + CBMonthBegin(n=1, offset=timedelta(days=-5)), + { + datetime(2021, 3, 1): datetime(2021, 4, 1) - timedelta(days=5), + datetime(2021, 4, 11): datetime(2021, 5, 3) - timedelta(days=5), + }, + ), + ( + -2 * CBMonthBegin(n=1, offset=timedelta(days=10)), + { + datetime(2021, 3, 1): datetime(2021, 1, 1) + timedelta(days=10), + datetime(2021, 4, 3): datetime(2021, 3, 1) + timedelta(days=10), + }, + ), + ( + CBMonthBegin(n=0, offset=timedelta(days=1)), + { + datetime(2021, 3, 2): datetime(2021, 4, 1) + timedelta(days=1), + datetime(2021, 4, 1): datetime(2021, 4, 1) + timedelta(days=1), + }, + ), + ( + CBMonthBegin( + n=1, holidays=["2021-04-01", "2021-04-02"], offset=timedelta(days=1) + ), + { + datetime(2021, 3, 2): datetime(2021, 4, 5) + timedelta(days=1), + }, + ), + ], + ) + def test_apply_with_extra_offset(self, case): + offset, cases = case + for base, expected in cases.items(): + assert_offset_equal(offset, base, expected) + class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base): _offset = CBMonthEnd @@ -337,3 +391,54 @@ def test_datetimeindex(self): assert date_range(start="20120101", end="20130101", freq=freq).tolist()[ 0 ] == datetime(2012, 1, 31) + + @pytest.mark.parametrize( + "case", + [ + ( + CBMonthEnd(n=1, offset=timedelta(days=5)), + { + datetime(2021, 3, 1): datetime(2021, 3, 31) + timedelta(days=5), + datetime(2021, 4, 17): datetime(2021, 4, 30) + timedelta(days=5), + }, + ), + ( + CBMonthEnd(n=2, offset=timedelta(days=40)), + { + datetime(2021, 3, 10): datetime(2021, 4, 30) + timedelta(days=40), + datetime(2021, 4, 30): datetime(2021, 6, 30) + timedelta(days=40), + }, + ), + ( + CBMonthEnd(n=1, offset=timedelta(days=-5)), + { + datetime(2021, 3, 1): datetime(2021, 3, 31) - timedelta(days=5), + datetime(2021, 4, 11): datetime(2021, 4, 30) - timedelta(days=5), + }, + ), + ( + -2 * CBMonthEnd(n=1, offset=timedelta(days=10)), + { + datetime(2021, 3, 1): datetime(2021, 1, 29) + timedelta(days=10), + datetime(2021, 4, 3): datetime(2021, 2, 26) + timedelta(days=10), + }, + ), + ( + CBMonthEnd(n=0, offset=timedelta(days=1)), + { + datetime(2021, 3, 2): datetime(2021, 3, 31) + timedelta(days=1), + datetime(2021, 4, 1): datetime(2021, 4, 30) + timedelta(days=1), + }, + ), + ( + CBMonthEnd(n=1, holidays=["2021-03-31"], offset=timedelta(days=1)), + { + datetime(2021, 3, 2): datetime(2021, 3, 30) + timedelta(days=1), + }, + ), + ], + ) + def test_apply_with_extra_offset(self, case): + offset, cases = case + for base, expected in cases.items(): + assert_offset_equal(offset, base, expected) From 05c8ee89140949f746e07502c400f8e6cbfa4fd4 Mon Sep 17 00:00:00 2001 From: Jack Liu <5811028+jackzyliu@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:02:25 -0700 Subject: [PATCH 2/2] whatsnew entry (GH41356) --- doc/source/whatsnew/v1.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index ae1844b0a913c..fbb39f0357370 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -281,6 +281,7 @@ Styler Other ^^^^^ +- Bug in :meth:`CustomBusinessMonthBegin.__add__` (:meth:`CustomBusinessMonthEnd.__add__`) not applying the extra ``offset`` parameter when beginning (end) of the target month is already a business day (:issue:`41356`) .. ***DO NOT USE THIS SECTION***