Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/docs/interval.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ If you want to iterate over a interval, you can use the `range()` method:
Supported units for `range()` are: `years`, `months`, `weeks`,
`days`, `hours`, `minutes`, `seconds` and `microseconds`

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

```python
>>> for dt in interval.range('days', 2):
Expand Down
3 changes: 3 additions & 0 deletions src/pendulum/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ def in_words(self, locale: str | None = None, separator: str = " ") -> str:
return separator.join(parts)

def range(self, unit: str, amount: int = 1) -> Iterator[_T]:
if amount <= 0:
raise ValueError("amount must be positive")

method = "add"
op = operator.le
if not self._absolute and self.invert:
Expand Down
57 changes: 57 additions & 0 deletions tests/interval/test_range_amount_regression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

import pendulum


if TYPE_CHECKING:
from collections.abc import Callable


@pytest.mark.parametrize("factory", [pendulum.date, pendulum.datetime])
@pytest.mark.parametrize("reverse", [False, True])
@pytest.mark.parametrize("absolute", [False, True])
@pytest.mark.parametrize("amount", [0, -1, -2])
def test_range_rejects_nonpositive_amount(
factory: Callable[[int, int, int], pendulum.Date],
reverse: bool,
absolute: bool,
amount: int,
) -> None:
start = factory(2024, 1, 10)
end = factory(2024, 1, 14)
if reverse:
start, end = end, start
interval = pendulum.Interval(start, end, absolute=absolute)
with pytest.raises(ValueError, match="amount must be positive"):
next(interval.range("days", amount))


@pytest.mark.parametrize("amount", [0, -1])
def test_single_point_range_rejects_nonpositive_amount(amount: int) -> None:
point = pendulum.date(2024, 1, 10)
interval = pendulum.Interval(point, point)
with pytest.raises(ValueError, match="amount must be positive"):
next(interval.range("days", amount))


@pytest.mark.parametrize("factory", [pendulum.date, pendulum.datetime])
@pytest.mark.parametrize("reverse", [False, True])
@pytest.mark.parametrize("amount", [1, 2, 3, 5])
def test_positive_range_amount_uses_interval_direction(
factory: Callable[[int, int, int], pendulum.Date], reverse: bool, amount: int
) -> None:
start = factory(2024, 1, 10)
end = factory(2024, 1, 14)
if reverse:
start, end = end, start
interval = pendulum.Interval(start, end)
direction = -1 if reverse else 1
expected = [
factory(2024, 1, day)
for day in range(start.day, end.day + direction, direction * amount)
]
assert list(interval.range("days", amount)) == expected
Loading