Skip to content

Commit 8882e94

Browse files
committed
Include dates inside inverted intervals
1 parent e06dace commit 8882e94

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

‎docs/docs/interval.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,6 @@ You can check if a `DateTime` instance is inside a interval using the `in` keywo
166166
>>> dt in interval
167167
True
168168
```
169+
170+
Membership includes both endpoints, including for inverted intervals. The order of
171+
the endpoints controls iteration direction, not whether a date is inside the interval.

‎src/pendulum/interval.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,10 @@ def __iter__(self) -> Iterator[_T]:
314314
return self.range("days")
315315

316316
def __contains__(self, item: _T) -> bool:
317-
return self.start <= item <= self.end
317+
if self.start <= self.end:
318+
return self.start <= item <= self.end
319+
320+
return self.end <= item <= self.start
318321

319322
def __add__(self, other: timedelta) -> Duration: # type: ignore[override]
320323
return self.as_duration().__add__(other)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
from datetime import date
4+
from datetime import datetime
5+
from datetime import timezone
6+
from functools import partial
7+
from typing import TYPE_CHECKING
8+
9+
import pytest
10+
11+
import pendulum
12+
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Callable
16+
17+
18+
@pytest.mark.parametrize(
19+
"factory",
20+
[date, partial(datetime, tzinfo=timezone.utc), pendulum.date, pendulum.datetime],
21+
)
22+
@pytest.mark.parametrize("reverse", [False, True])
23+
@pytest.mark.parametrize("absolute", [False, True])
24+
def test_interval_membership_includes_both_endpoints(
25+
factory: Callable[[int, int, int], date], reverse: bool, absolute: bool
26+
) -> None:
27+
first = factory(2024, 1, 10)
28+
last = factory(2024, 1, 12)
29+
start, end = (last, first) if reverse else (first, last)
30+
interval = pendulum.Interval(start, end, absolute=absolute)
31+
32+
for day in (10, 11, 12):
33+
assert factory(2024, 1, day) in interval
34+
for day in (9, 13):
35+
assert factory(2024, 1, day) not in interval
36+
37+
assert interval.start == (first if absolute else start)
38+
assert interval.end == (last if absolute else end)
39+
40+
41+
@pytest.mark.parametrize(
42+
"factory",
43+
[date, partial(datetime, tzinfo=timezone.utc), pendulum.date, pendulum.datetime],
44+
)
45+
def test_single_point_interval_membership(
46+
factory: Callable[[int, int, int], date],
47+
) -> None:
48+
point = factory(2024, 1, 10)
49+
interval = pendulum.Interval(point, point)
50+
assert point in interval
51+
assert factory(2024, 1, 9) not in interval
52+
assert factory(2024, 1, 11) not in interval
53+
54+
55+
def test_inverted_interval_membership_with_different_offsets() -> None:
56+
start = pendulum.datetime(2024, 1, 10, 16, tz="Europe/Paris")
57+
end = pendulum.datetime(2024, 1, 10, 9, tz="America/New_York")
58+
interval = pendulum.Interval(start, end)
59+
assert pendulum.datetime(2024, 1, 10, 14, 30, tz="UTC") in interval
60+
assert pendulum.datetime(2024, 1, 10, 13, 59, tz="UTC") not in interval
61+
assert pendulum.datetime(2024, 1, 10, 15, 1, tz="UTC") not in interval

0 commit comments

Comments
 (0)