Skip to content

Commit 203733e

Browse files
committed
Change date_range inclusive
1 parent 95a3b07 commit 203733e

File tree

7 files changed

+201
-45
lines changed

7 files changed

+201
-45
lines changed

doc/source/whatsnew/v1.4.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ Other Deprecations
299299
- 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`)
300300
- Deprecated the ``index`` argument to :class:`SparseArray` construction (:issue:`23089`)
301301
-
302+
- 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`)
302303

303304
.. ---------------------------------------------------------------------------
304305
@@ -340,6 +341,7 @@ Datetimelike
340341
^^^^^^^^^^^^
341342
- Bug in :class:`DataFrame` constructor unnecessarily copying non-datetimelike 2D object arrays (:issue:`39272`)
342343
- :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`)
344+
- 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`)
343345
-
344346

345347
Timedelta

pandas/core/arrays/datetimelike.py

+65
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,71 @@ def validate_periods(periods):
18231823
return periods
18241824

18251825

1826+
def validate_inclusiveness(inclusive):
1827+
"""
1828+
Check that the `inclusive` argument is among {"both", "neither", "left", "right"}.
1829+
1830+
Parameters
1831+
----------
1832+
inclusive : {"both", "neither", "left", "right"}
1833+
1834+
Returns
1835+
-------
1836+
left_inclusive : bool
1837+
right_inclusive : bool
1838+
1839+
Raises
1840+
------
1841+
ValueError : if argument is not among valid values
1842+
"""
1843+
left_right_inclusive: tuple[bool, bool] | None = {
1844+
"both": (True, True),
1845+
"left": (True, False),
1846+
"right": (False, True),
1847+
"neither": (False, False),
1848+
}.get(inclusive)
1849+
1850+
if left_right_inclusive is None:
1851+
raise ValueError(
1852+
"Inclusive has to be either 'both', 'neither', 'left', 'right'"
1853+
)
1854+
left_inclusive, right_inclusive = left_right_inclusive
1855+
return left_inclusive, right_inclusive
1856+
1857+
1858+
def validate_endpoints(closed):
1859+
"""
1860+
Check that the `closed` argument is among [None, "left", "right"]
1861+
1862+
Parameters
1863+
----------
1864+
closed : {None, "left", "right"}
1865+
1866+
Returns
1867+
-------
1868+
left_closed : bool
1869+
right_closed : bool
1870+
1871+
Raises
1872+
------
1873+
ValueError : if argument is not among valid values
1874+
"""
1875+
left_closed = False
1876+
right_closed = False
1877+
1878+
if closed is None:
1879+
left_closed = True
1880+
right_closed = True
1881+
elif closed == "left":
1882+
left_closed = True
1883+
elif closed == "right":
1884+
right_closed = True
1885+
else:
1886+
raise ValueError("Closed has to be either 'left', 'right' or None")
1887+
1888+
return left_closed, right_closed
1889+
1890+
18261891
def validate_inferred_freq(freq, inferred_freq, freq_infer):
18271892
"""
18281893
If the user passes a freq and another freq is inferred from passed data,

pandas/core/arrays/datetimes.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -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 = dtl.validate_inclusiveness(inclusive)
421421
start, end, _normalized = _maybe_normalize_endpoints(start, end, normalize)
422422
tz = _infer_tz_from_endpoints(start, end, tz)
423423

@@ -477,10 +477,20 @@ 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+
# do not remove when one side is inclusive
481+
# and removing would leave index empty
482+
to_remove_any = not (
483+
(left_inclusive or right_inclusive)
484+
and len(index) == 1
485+
and start == index[0]
486+
and start == end
487+
)
488+
489+
if to_remove_any:
490+
if (not left_inclusive) and len(index) and index[0] == start:
491+
index = index[1:]
492+
if (not right_inclusive) and len(index) and index[-1] == end:
493+
index = index[:-1]
484494

485495
dtype = tz_to_dtype(tz)
486496
return cls._simple_new(index._ndarray, freq=freq, dtype=dtype)

pandas/core/indexes/datetimes.py

+40-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: bool | lib.NoDefault = lib.no_default,
885+
inclusive: str | None = None,
885886
**kwargs,
886887
) -> DatetimeIndex:
887888
"""
@@ -919,6 +920,12 @@ 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+
.. deprecated:: 1.4.0
924+
Argument `closed` have been deprecated
925+
to standardize boundary inputs. Use `inclusive` instead, to set
926+
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.
922929
**kwargs
923930
For compatibility. Has no effect on the result.
924931
@@ -1029,6 +1036,28 @@ def date_range(
10291036
DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'],
10301037
dtype='datetime64[ns]', freq='D')
10311038
"""
1039+
if inclusive is not None and closed is not lib.no_default:
1040+
raise ValueError(
1041+
"Deprecated argument `closed` cannot be passed"
1042+
"if argument `inclusive` is not None"
1043+
)
1044+
elif closed is not lib.no_default:
1045+
warnings.warn(
1046+
"Argument `closed` is deprecated in favor of `inclusive`.",
1047+
FutureWarning,
1048+
stacklevel=2,
1049+
)
1050+
if closed is None:
1051+
inclusive = "both"
1052+
elif closed in ("left", "right"):
1053+
inclusive = closed
1054+
else:
1055+
raise ValueError(
1056+
"Argument `closed` has to be either 'left', 'right' or None"
1057+
)
1058+
elif inclusive is None:
1059+
inclusive = "both"
1060+
10321061
if freq is None and com.any_none(periods, start, end):
10331062
freq = "D"
10341063

@@ -1039,7 +1068,7 @@ def date_range(
10391068
freq=freq,
10401069
tz=tz,
10411070
normalize=normalize,
1042-
closed=closed,
1071+
inclusive=inclusive,
10431072
**kwargs,
10441073
)
10451074
return DatetimeIndex._simple_new(dtarr, name=name)
@@ -1055,7 +1084,8 @@ def bdate_range(
10551084
name: Hashable = None,
10561085
weekmask=None,
10571086
holidays=None,
1058-
closed=None,
1087+
closed: lib.NoDefault = lib.no_default,
1088+
inclusive: str | None = None,
10591089
**kwargs,
10601090
) -> DatetimeIndex:
10611091
"""
@@ -1090,6 +1120,12 @@ def bdate_range(
10901120
closed : str, default None
10911121
Make the interval closed with respect to the given frequency to
10921122
the 'left', 'right', or both sides (None).
1123+
.. deprecated:: 1.4.0
1124+
Argument `closed` have been deprecated
1125+
to standardize boundary inputs. Use `inclusive` instead, to set
1126+
each bound as closed or open.
1127+
inclusive : {"both", "neither", "left", "right"}, default "both"
1128+
Include boundaries; Whether to set each bound as closed or open.
10931129
**kwargs
10941130
For compatibility. Has no effect on the result.
10951131
@@ -1143,6 +1179,7 @@ def bdate_range(
11431179
normalize=normalize,
11441180
name=name,
11451181
closed=closed,
1182+
inclusive=inclusive,
11461183
**kwargs,
11471184
)
11481185

0 commit comments

Comments
 (0)