From 3a1e6dd7c15bbd29c123b7a85e8019b40a44e36e Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 11 May 2026 13:08:43 -0700 Subject: [PATCH 1/2] BUG: bdate_range(end, periods, freq=nB) anchors stride from start of buffer Follow-up to GH-64648: when end + periods are given and freq is n*B (or n*C) with n >= 2, the [::abs_n] stride was anchored at the start of the internal buffer instead of at end, producing dates offset by (buffer_days % n). Reverse before striding so the anchor is the last on-offset date <= end. Co-Authored-By: Claude Opus 4.7 (1M context) --- pandas/core/arrays/_ranges.py | 8 +++++++- pandas/tests/indexes/datetimes/test_date_range.py | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/_ranges.py b/pandas/core/arrays/_ranges.py index 48ca71c0c11e4..878bf34bcdd1d 100644 --- a/pandas/core/arrays/_ranges.py +++ b/pandas/core/arrays/_ranges.py @@ -152,7 +152,13 @@ def generate_daily_offset_range( i8values = i8values[freq._get_daily_offset_mask(dt64)] # type: ignore[attr-defined] if abs_n > 1: - i8values = i8values[::abs_n] + # When trimming from the end (end+periods case), anchor stride on + # the last on-offset date so e.g. freq="2B" with end on a Sunday + # yields the last business day, last-2, last-4, ... (GH#64834). + if trim_from_end: + i8values = i8values[::-1][::abs_n][::-1] + else: + i8values = i8values[::abs_n] if trim_to is not None: if trim_from_end: diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 8c52d2c7a71a6..9a31e01da2152 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -1194,6 +1194,19 @@ def test_bdate_range_end_weekend_periods(self): result = bdate_range(end="2026-03-22", periods=3) tm.assert_index_equal(result, expected) + @pytest.mark.parametrize("end", ["2026-03-20", "2026-03-21", "2026-03-22"]) + def test_bdate_range_end_periods_multiple_n(self, end): + # GH#64648 (post-merge): with end+periods and freq="nB" (n>=2), + # the on-offset stride must be anchored at the end (last business + # day <= end), not at the start of the internal buffer. + result = bdate_range(end=end, periods=3, freq="2B") + expected = DatetimeIndex(["2026-03-16", "2026-03-18", "2026-03-20"]) + tm.assert_index_equal(result, expected) + + result = bdate_range(end=end, periods=3, freq="3B") + expected = DatetimeIndex(["2026-03-12", "2026-03-17", "2026-03-20"]) + tm.assert_index_equal(result, expected) + class TestCustomDateRange: def test_constructor(self): From 69327c70cd23a8a3afb0600455b866fa706324de Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 28 May 2026 16:25:31 -0700 Subject: [PATCH 2/2] CLN: fix issue reference in generate_daily_offset_range comment The stride-anchoring comment cited GH#64834, which is a separate (already-fixed) n=1 weekend regression. Point it at GH#64648, the PR that introduced this fast path, matching the commit and test references. Also drop the weekend-specific framing since the bug occurs even when end is itself a business day. Co-Authored-By: Claude Opus 4.8 (1M context) --- pandas/core/arrays/_ranges.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/_ranges.py b/pandas/core/arrays/_ranges.py index 878bf34bcdd1d..bcf453956e55b 100644 --- a/pandas/core/arrays/_ranges.py +++ b/pandas/core/arrays/_ranges.py @@ -152,9 +152,11 @@ def generate_daily_offset_range( i8values = i8values[freq._get_daily_offset_mask(dt64)] # type: ignore[attr-defined] if abs_n > 1: - # When trimming from the end (end+periods case), anchor stride on - # the last on-offset date so e.g. freq="2B" with end on a Sunday - # yields the last business day, last-2, last-4, ... (GH#64834). + # When trimming from the end (end+periods case), the buffer starts + # at an arbitrary date (end - buffer_days), so a forward [::abs_n] + # stride would anchor there. Reverse first so the stride is anchored + # on the last on-offset date <= end, e.g. freq="2B" yields + # ..., last-4, last-2, last business day (GH#64648). if trim_from_end: i8values = i8values[::-1][::abs_n][::-1] else: