Skip to content

Commit 1b04b36

Browse files
authored
feat(python): Improve Expr.is_between API (#5981)
1 parent d5793fd commit 1b04b36

File tree

3 files changed

+70
-30
lines changed

3 files changed

+70
-30
lines changed

py-polars/polars/internals/expr/expr.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3327,7 +3327,8 @@ def is_between(
33273327
self,
33283328
start: Expr | datetime | date | int | float,
33293329
end: Expr | datetime | date | int | float,
3330-
include_bounds: bool | tuple[bool, bool] = False,
3330+
include_bounds: bool | tuple[bool, bool] | None = None,
3331+
closed: ClosedWindow | None = None,
33313332
) -> Expr:
33323333
"""
33333334
Check if this expression is between start and end.
@@ -3339,12 +3340,16 @@ def is_between(
33393340
end
33403341
Upper bound as primitive type or datetime.
33413342
include_bounds
3342-
False: Exclude both start and end (default).
3343-
True: Include both start and end.
3344-
(False, False): Exclude start and exclude end.
3345-
(True, True): Include start and include end.
3346-
(False, True): Exclude start and include end.
3347-
(True, False): Include start and exclude end.
3343+
This argument is deprecated. Use ``closed`` instead!
3344+
3345+
- False: Exclude both start and end (default).
3346+
- True: Include both start and end.
3347+
- (False, False): Exclude start and exclude end.
3348+
- (True, True): Include start and include end.
3349+
- (False, True): Exclude start and include end.
3350+
- (True, False): Include start and exclude end.
3351+
closed : {'none', 'left', 'right', 'both'}
3352+
Define whether the interval is closed or not. Defaults to 'none'.
33483353
33493354
Returns
33503355
-------
@@ -3367,25 +3372,67 @@ def is_between(
33673372
│ 5 ┆ false │
33683373
└─────┴────────────┘
33693374
3375+
Use the ``closed`` argument to include or exclude the values at the bounds.
3376+
3377+
>>> df.with_column(pl.col("num").is_between(2, 4, closed="left"))
3378+
shape: (5, 2)
3379+
┌─────┬────────────┐
3380+
│ num ┆ is_between │
3381+
│ --- ┆ --- │
3382+
│ i64 ┆ bool │
3383+
╞═════╪════════════╡
3384+
│ 1 ┆ false │
3385+
│ 2 ┆ true │
3386+
│ 3 ┆ true │
3387+
│ 4 ┆ false │
3388+
│ 5 ┆ false │
3389+
└─────┴────────────┘
3390+
33703391
"""
3371-
if isinstance(include_bounds, list):
3392+
if include_bounds is not None:
33723393
warnings.warn(
3373-
"include_bounds: list[bool] will not be supported in a future "
3374-
"version; pass include_bounds: tuple[bool, bool] instead",
3394+
"The `include_bounds` argument will be replaced in a future version."
3395+
" Use the `closed` argument to silence this warning.",
33753396
category=DeprecationWarning,
33763397
)
3377-
include_bounds = tuple(include_bounds)
3398+
if isinstance(include_bounds, list):
3399+
include_bounds = tuple(include_bounds)
3400+
3401+
if include_bounds is False or include_bounds == (False, False):
3402+
closed = "none"
3403+
elif include_bounds is True or include_bounds == (True, True):
3404+
closed = "both"
3405+
elif include_bounds == (False, True):
3406+
closed = "right"
3407+
elif include_bounds == (True, False):
3408+
closed = "left"
3409+
else:
3410+
raise ValueError(
3411+
"include_bounds should be a bool or tuple[bool, bool]."
3412+
)
3413+
3414+
if closed is None:
3415+
warnings.warn(
3416+
"Default behaviour will change from excluding both bounds to including"
3417+
" both bounds. Provide a value for the `closed` argument to silence"
3418+
" this warning.",
3419+
category=FutureWarning,
3420+
)
3421+
closed = "none"
33783422

3379-
if include_bounds is False or include_bounds == (False, False):
3423+
if closed == "none":
33803424
return ((self > start) & (self < end)).alias("is_between")
3381-
elif include_bounds is True or include_bounds == (True, True):
3425+
elif closed == "both":
33823426
return ((self >= start) & (self <= end)).alias("is_between")
3383-
elif include_bounds == (False, True):
3427+
elif closed == "right":
33843428
return ((self > start) & (self <= end)).alias("is_between")
3385-
elif include_bounds == (True, False):
3429+
elif closed == "left":
33863430
return ((self >= start) & (self < end)).alias("is_between")
33873431
else:
3388-
raise ValueError("include_bounds should be a bool or tuple[bool, bool].")
3432+
raise ValueError(
3433+
"closed must be one of {'left', 'right', 'both', 'none'},"
3434+
f" got {closed}"
3435+
)
33893436

33903437
def hash(
33913438
self,

py-polars/tests/unit/test_lazy.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,51 +1168,43 @@ def test_quantile(fruits_cars: pl.DataFrame) -> None:
11681168
assert fruits_cars.select(pl.col("A").quantile(0.24, "linear"))["A"][0] == 1.96
11691169

11701170

1171+
@pytest.mark.filterwarnings("ignore::FutureWarning")
11711172
def test_is_between(fruits_cars: pl.DataFrame) -> None:
11721173
result = fruits_cars.select(pl.col("A").is_between(2, 4))["is_between"]
11731174
assert result.series_equal(
11741175
pl.Series("is_between", [False, False, True, False, False])
11751176
)
11761177

1177-
result = fruits_cars.select(pl.col("A").is_between(2, 4, False))["is_between"]
1178-
assert result.series_equal(
1179-
pl.Series("is_between", [False, False, True, False, False])
1180-
)
1181-
1182-
result = fruits_cars.select(pl.col("A").is_between(2, 4, (False, False)))[
1178+
result = fruits_cars.select(pl.col("A").is_between(2, 4, closed="none"))[
11831179
"is_between"
11841180
]
11851181
assert result.series_equal(
11861182
pl.Series("is_between", [False, False, True, False, False])
11871183
)
11881184

1189-
result = fruits_cars.select(pl.col("A").is_between(2, 4, True))["is_between"]
1190-
assert result.series_equal(
1191-
pl.Series("is_between", [False, True, True, True, False])
1192-
)
1193-
1194-
result = fruits_cars.select(pl.col("A").is_between(2, 4, (True, True)))[
1185+
result = fruits_cars.select(pl.col("A").is_between(2, 4, closed="both"))[
11951186
"is_between"
11961187
]
11971188
assert result.series_equal(
11981189
pl.Series("is_between", [False, True, True, True, False])
11991190
)
12001191

1201-
result = fruits_cars.select(pl.col("A").is_between(2, 4, (False, True)))[
1192+
result = fruits_cars.select(pl.col("A").is_between(2, 4, closed="right"))[
12021193
"is_between"
12031194
]
12041195
assert result.series_equal(
12051196
pl.Series("is_between", [False, False, True, True, False])
12061197
)
12071198

1208-
result = fruits_cars.select(pl.col("A").is_between(2, 4, (True, False)))[
1199+
result = fruits_cars.select(pl.col("A").is_between(2, 4, closed="left"))[
12091200
"is_between"
12101201
]
12111202
assert result.series_equal(
12121203
pl.Series("is_between", [False, True, True, False, False])
12131204
)
12141205

12151206

1207+
@pytest.mark.filterwarnings("ignore::FutureWarning")
12161208
def test_is_between_data_types() -> None:
12171209
df = pl.DataFrame(
12181210
{

py-polars/tests/unit/test_series.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,6 +2050,7 @@ def test_to_physical() -> None:
20502050
verify_series_and_expr_api(a, expected, "to_physical")
20512051

20522052

2053+
@pytest.mark.filterwarnings("ignore::FutureWarning")
20532054
def test_is_between_datetime() -> None:
20542055
s = pl.Series("a", [datetime(2020, 1, 1, 10, 0, 0), datetime(2020, 1, 1, 20, 0, 0)])
20552056
start = datetime(2020, 1, 1, 12, 0, 0)

0 commit comments

Comments
 (0)