diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 63d85693ef548..4ea9c09bfd14a 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -301,6 +301,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ - Bug in :func:`date_range` where the last valid timestamp would sometimes not be produced (:issue:`56134`) +- Bug in :func:`date_range` where using a negative frequency value would not include all points between the start and end values (:issue:`56382`) - Timedelta diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index e4862ac1030b6..ad4611aac9e35 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -2777,7 +2777,12 @@ def _generate_range( if start and not offset.is_on_offset(start): # Incompatible types in assignment (expression has type "datetime", # variable has type "Optional[Timestamp]") - start = offset.rollforward(start) # type: ignore[assignment] + + # GH #56147 account for negative direction and range bounds + if offset.n >= 0: + start = offset.rollforward(start) # type: ignore[assignment] + else: + start = offset.rollback(start) # type: ignore[assignment] # Unsupported operand types for < ("Timestamp" and "None") if periods is None and end < start and offset.n >= 0: # type: ignore[operator] diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 4f9c810cc7e1d..2d773c04b8ea9 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -841,13 +841,15 @@ def date_range( Return a fixed frequency DatetimeIndex. Returns the range of equally spaced time points (where the difference between any - two adjacent points is specified by the given frequency) such that they all - satisfy `start <[=] x <[=] end`, where the first one and the last one are, resp., - the first and last time points in that range that fall on the boundary of ``freq`` - (if given as a frequency string) or that are valid for ``freq`` (if given as a - :class:`pandas.tseries.offsets.DateOffset`). (If exactly one of ``start``, - ``end``, or ``freq`` is *not* specified, this missing parameter can be computed - given ``periods``, the number of timesteps in the range. See the note below.) + two adjacent points is specified by the given frequency) such that they fall in the + range `[start, end]` , where the first one and the last one are, resp., the first + and last time points in that range that fall on the boundary of ``freq`` (if given + as a frequency string) or that are valid for ``freq`` (if given as a + :class:`pandas.tseries.offsets.DateOffset`). If ``freq`` is positive, the points + satisfy `start <[=] x <[=] end`, and if ``freq`` is negative, the points satisfy + `end <[=] x <[=] start`. (If exactly one of ``start``, ``end``, or ``freq`` is *not* + specified, this missing parameter can be computed given ``periods``, the number of + timesteps in the range. See the note below.) Parameters ---------- diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index fecd7f4e7f2b0..ddbeecf150a5e 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -1735,3 +1735,18 @@ def test_date_range_partial_day_year_end(self, unit): freq="YE", ) tm.assert_index_equal(rng, exp) + + def test_date_range_negative_freq_year_end_inbounds(self, unit): + # GH#56147 + rng = date_range( + start="2023-10-31 00:00:00", + end="2021-10-31 00:00:00", + freq="-1YE", + unit=unit, + ) + exp = DatetimeIndex( + ["2022-12-31 00:00:00", "2021-12-31 00:00:00"], + dtype=f"M8[{unit}]", + freq="-1YE", + ) + tm.assert_index_equal(rng, exp)