Skip to content

Commit 3db765a

Browse files
zyc09attack68
andauthored
ENH: consistency of input args for boundaries (pd.date_range) (#43504)
Co-authored-by: JHM Darbyshire <[email protected]>
1 parent 4463fb1 commit 3db765a

File tree

9 files changed

+192
-72
lines changed

9 files changed

+192
-72
lines changed

doc/source/whatsnew/v1.4.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ Other Deprecations
336336
- Deprecated the 'include_start' and 'include_end' arguments in :meth:`DataFrame.between_time`; in a future version passing 'include_start' or 'include_end' will raise (:issue:`40245`)
337337
- Deprecated the ``squeeze`` argument to :meth:`read_csv`, :meth:`read_table`, and :meth:`read_excel`. Users should squeeze the DataFrame afterwards with ``.squeeze("columns")`` instead. (:issue:`43242`)
338338
- Deprecated the ``index`` argument to :class:`SparseArray` construction (:issue:`23089`)
339+
- Deprecated the ``closed`` argument in :meth:`date_range` and :meth:`bdate_range` in favor of ``inclusive`` argument; In a future version passing ``closed`` will raise (:issue:`40245`)
339340
- Deprecated :meth:`.Rolling.validate`, :meth:`.Expanding.validate`, and :meth:`.ExponentialMovingWindow.validate` (:issue:`43665`)
340341
- Deprecated silent dropping of columns that raised a ``TypeError`` in :class:`Series.transform` and :class:`DataFrame.transform` when used with a dictionary (:issue:`43740`)
341342
- Deprecated silent dropping of columns that raised a ``TypeError``, ``DataError``, and some cases of ``ValueError`` in :meth:`Series.aggregate`, :meth:`DataFrame.aggregate`, :meth:`Series.groupby.aggregate`, and :meth:`DataFrame.groupby.aggregate` when used with a list (:issue:`43740`)
@@ -385,6 +386,7 @@ Datetimelike
385386
- Bug in :class:`DataFrame` constructor unnecessarily copying non-datetimelike 2D object arrays (:issue:`39272`)
386387
- Bug in :func:`to_datetime` with ``format`` and ``pandas.NA`` was raising ``ValueError`` (:issue:`42957`)
387388
- :func:`to_datetime` would silently swap ``MM/DD/YYYY`` and ``DD/MM/YYYY`` formats if the given ``dayfirst`` option could not be respected - now, a warning is raised in the case of delimited date strings (e.g. ``31-12-2012``) (:issue:`12585`)
389+
- Bug in :meth:`date_range` and :meth:`bdate_range` do not return right bound when ``start`` = ``end`` and set is closed on one side (:issue:`43394`)
388390
-
389391

390392
Timedelta

pandas/conftest.py

+8
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ def keep(request):
242242
return request.param
243243

244244

245+
@pytest.fixture(params=["both", "neither", "left", "right"])
246+
def inclusive_endpoints_fixture(request):
247+
"""
248+
Fixture for trying all interval 'inclusive' parameters.
249+
"""
250+
return request.param
251+
252+
245253
@pytest.fixture(params=["left", "right", "both", "neither"])
246254
def closed(request):
247255
"""

pandas/core/arrays/datetimes.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
)
4040
from pandas._typing import npt
4141
from pandas.errors import PerformanceWarning
42-
from pandas.util._validators import validate_endpoints
42+
from pandas.util._validators import validate_inclusive
4343

4444
from pandas.core.dtypes.cast import astype_dt64_to_dt64tz
4545
from pandas.core.dtypes.common import (
@@ -394,7 +394,7 @@ def _generate_range(
394394
normalize=False,
395395
ambiguous="raise",
396396
nonexistent="raise",
397-
closed=None,
397+
inclusive="both",
398398
):
399399

400400
periods = dtl.validate_periods(periods)
@@ -417,7 +417,7 @@ def _generate_range(
417417
if start is NaT or end is NaT:
418418
raise ValueError("Neither `start` nor `end` can be NaT")
419419

420-
left_closed, right_closed = validate_endpoints(closed)
420+
left_inclusive, right_inclusive = validate_inclusive(inclusive)
421421
start, end, _normalized = _maybe_normalize_endpoints(start, end, normalize)
422422
tz = _infer_tz_from_endpoints(start, end, tz)
423423

@@ -477,10 +477,15 @@ def _generate_range(
477477
arr = arr.astype("M8[ns]", copy=False)
478478
index = cls._simple_new(arr, freq=None, dtype=dtype)
479479

480-
if not left_closed and len(index) and index[0] == start:
481-
index = index[1:]
482-
if not right_closed and len(index) and index[-1] == end:
483-
index = index[:-1]
480+
if start == end:
481+
if not left_inclusive and not right_inclusive:
482+
index = index[1:-1]
483+
else:
484+
if not left_inclusive or not right_inclusive:
485+
if not left_inclusive and len(index) and index[0] == start:
486+
index = index[1:]
487+
if not right_inclusive and len(index) and index[-1] == end:
488+
index = index[:-1]
484489

485490
dtype = tz_to_dtype(tz)
486491
return cls._simple_new(index._ndarray, freq=freq, dtype=dtype)

pandas/core/indexes/datetimes.py

+44-3
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,8 @@ def date_range(
881881
tz=None,
882882
normalize: bool = False,
883883
name: Hashable = None,
884-
closed=None,
884+
closed: str | None | lib.NoDefault = lib.no_default,
885+
inclusive: str | None = None,
885886
**kwargs,
886887
) -> DatetimeIndex:
887888
"""
@@ -919,6 +920,14 @@ def date_range(
919920
closed : {None, 'left', 'right'}, optional
920921
Make the interval closed with respect to the given frequency to
921922
the 'left', 'right', or both sides (None, the default).
923+
924+
.. deprecated:: 1.4.0
925+
Argument `closed` has been deprecated to standardize boundary inputs.
926+
Use `inclusive` instead, to set each bound as closed or open.
927+
inclusive : {"both", "neither", "left", "right"}, default "both"
928+
Include boundaries; Whether to set each bound as closed or open.
929+
930+
.. versionadded:: 1.4.0
922931
**kwargs
923932
For compatibility. Has no effect on the result.
924933
@@ -1029,6 +1038,28 @@ def date_range(
10291038
DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'],
10301039
dtype='datetime64[ns]', freq='D')
10311040
"""
1041+
if inclusive is not None and not isinstance(closed, lib.NoDefault):
1042+
raise ValueError(
1043+
"Deprecated argument `closed` cannot be passed"
1044+
"if argument `inclusive` is not None"
1045+
)
1046+
elif not isinstance(closed, lib.NoDefault):
1047+
warnings.warn(
1048+
"Argument `closed` is deprecated in favor of `inclusive`.",
1049+
FutureWarning,
1050+
stacklevel=2,
1051+
)
1052+
if closed is None:
1053+
inclusive = "both"
1054+
elif closed in ("left", "right"):
1055+
inclusive = closed
1056+
else:
1057+
raise ValueError(
1058+
"Argument `closed` has to be either 'left', 'right' or None"
1059+
)
1060+
elif inclusive is None:
1061+
inclusive = "both"
1062+
10321063
if freq is None and com.any_none(periods, start, end):
10331064
freq = "D"
10341065

@@ -1039,7 +1070,7 @@ def date_range(
10391070
freq=freq,
10401071
tz=tz,
10411072
normalize=normalize,
1042-
closed=closed,
1073+
inclusive=inclusive,
10431074
**kwargs,
10441075
)
10451076
return DatetimeIndex._simple_new(dtarr, name=name)
@@ -1055,7 +1086,8 @@ def bdate_range(
10551086
name: Hashable = None,
10561087
weekmask=None,
10571088
holidays=None,
1058-
closed=None,
1089+
closed: lib.NoDefault = lib.no_default,
1090+
inclusive: str | None = None,
10591091
**kwargs,
10601092
) -> DatetimeIndex:
10611093
"""
@@ -1090,6 +1122,14 @@ def bdate_range(
10901122
closed : str, default None
10911123
Make the interval closed with respect to the given frequency to
10921124
the 'left', 'right', or both sides (None).
1125+
1126+
.. deprecated:: 1.4.0
1127+
Argument `closed` has been deprecated to standardize boundary inputs.
1128+
Use `inclusive` instead, to set each bound as closed or open.
1129+
inclusive : {"both", "neither", "left", "right"}, default "both"
1130+
Include boundaries; Whether to set each bound as closed or open.
1131+
1132+
.. versionadded:: 1.4.0
10931133
**kwargs
10941134
For compatibility. Has no effect on the result.
10951135
@@ -1143,6 +1183,7 @@ def bdate_range(
11431183
normalize=normalize,
11441184
name=name,
11451185
closed=closed,
1186+
inclusive=inclusive,
11461187
**kwargs,
11471188
)
11481189

pandas/tests/frame/conftest.py

-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99
import pandas._testing as tm
1010

1111

12-
@pytest.fixture(params=["both", "neither", "left", "right"])
13-
def inclusive_endpoints_fixture(request):
14-
return request.param
15-
16-
1712
@pytest.fixture
1813
def float_frame_with_na():
1914
"""

pandas/tests/groupby/test_timegrouper.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def test_groupby_with_timegrouper(self):
100100
expected = DataFrame(
101101
{"Quantity": 0},
102102
index=date_range(
103-
"20130901", "20131205", freq="5D", name="Date", closed="left"
103+
"20130901", "20131205", freq="5D", name="Date", inclusive="left"
104104
),
105105
)
106106
expected.iloc[[0, 6, 18], 0] = np.array([24, 6, 9], dtype="int64")

0 commit comments

Comments
 (0)