Skip to content

Commit c5e7814

Browse files
committed
Reject nonpositive interval range amounts
1 parent e06dace commit c5e7814

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

‎docs/docs/interval.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ If you want to iterate over a interval, you can use the `range()` method:
138138
Supported units for `range()` are: `years`, `months`, `weeks`,
139139
`days`, `hours`, `minutes`, `seconds` and `microseconds`
140140

141-
You can pass an amount for the passed unit to control the length of the gap:
141+
You can pass a positive amount for the passed unit to control the length of the gap.
142+
Zero or negative amounts raise `ValueError` when iteration starts. Inverted intervals
143+
already iterate backwards, so their amount must also be positive:
142144

143145
```python
144146
>>> for dt in interval.range('days', 2):

‎src/pendulum/interval.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ def in_words(self, locale: str | None = None, separator: str = " ") -> str:
288288
return separator.join(parts)
289289

290290
def range(self, unit: str, amount: int = 1) -> Iterator[_T]:
291+
if amount <= 0:
292+
raise ValueError("amount must be positive")
293+
291294
method = "add"
292295
op = operator.le
293296
if not self._absolute and self.invert:
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
7+
import pendulum
8+
9+
10+
if TYPE_CHECKING:
11+
from collections.abc import Callable
12+
13+
14+
@pytest.mark.parametrize("factory", [pendulum.date, pendulum.datetime])
15+
@pytest.mark.parametrize("reverse", [False, True])
16+
@pytest.mark.parametrize("absolute", [False, True])
17+
@pytest.mark.parametrize("amount", [0, -1, -2])
18+
def test_range_rejects_nonpositive_amount(
19+
factory: Callable[[int, int, int], pendulum.Date],
20+
reverse: bool,
21+
absolute: bool,
22+
amount: int,
23+
) -> None:
24+
start = factory(2024, 1, 10)
25+
end = factory(2024, 1, 14)
26+
if reverse:
27+
start, end = end, start
28+
interval = pendulum.Interval(start, end, absolute=absolute)
29+
with pytest.raises(ValueError, match="amount must be positive"):
30+
next(interval.range("days", amount))
31+
32+
33+
@pytest.mark.parametrize("amount", [0, -1])
34+
def test_single_point_range_rejects_nonpositive_amount(amount: int) -> None:
35+
point = pendulum.date(2024, 1, 10)
36+
interval = pendulum.Interval(point, point)
37+
with pytest.raises(ValueError, match="amount must be positive"):
38+
next(interval.range("days", amount))
39+
40+
41+
@pytest.mark.parametrize("factory", [pendulum.date, pendulum.datetime])
42+
@pytest.mark.parametrize("reverse", [False, True])
43+
@pytest.mark.parametrize("amount", [1, 2, 3, 5])
44+
def test_positive_range_amount_uses_interval_direction(
45+
factory: Callable[[int, int, int], pendulum.Date], reverse: bool, amount: int
46+
) -> None:
47+
start = factory(2024, 1, 10)
48+
end = factory(2024, 1, 14)
49+
if reverse:
50+
start, end = end, start
51+
interval = pendulum.Interval(start, end)
52+
direction = -1 if reverse else 1
53+
expected = [
54+
factory(2024, 1, day)
55+
for day in range(start.day, end.day + direction, direction * amount)
56+
]
57+
assert list(interval.range("days", amount)) == expected

0 commit comments

Comments
 (0)