Skip to content

Commit 69888d0

Browse files
committed
Fix Interval.range hang on a zero or wrong-sign amount
range() treated amount as both the step and the termination increment with no validation, so amount=0 yielded the start forever and a mismatched sign walked away from the end. Reject a zero amount with ValueError. A negative amount whose sign does not match the interval direction now yields an empty iterator, matching Python range().
1 parent e06dace commit 69888d0

3 files changed

Lines changed: 37 additions & 0 deletions

File tree

‎docs/docs/interval.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ You can pass an amount for the passed unit to control the length of the gap:
151151
'2000-01-09T00:00:00+00:00'
152152
```
153153

154+
`amount` must be a non-zero integer. A zero amount raises `ValueError`.
155+
A negative amount whose sign does not match the interval direction
156+
yields an empty iterator (the same way Python's `range()` does).
157+
154158
You can also directly iterate over the `Interval` instance,
155159
the unit will be `days` in this case:
156160

‎src/pendulum/interval.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,23 @@ 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("range() requires a non-zero amount")
293+
291294
method = "add"
292295
op = operator.le
293296
if not self._absolute and self.invert:
294297
method = "subtract"
295298
op = operator.ge
296299

300+
# A negative amount walks the opposite direction of the interval.
301+
# Flip the step so the loop either iterates toward the end (matching
302+
# sign) or is empty immediately (wrong sign), instead of hanging.
303+
if amount < 0:
304+
amount = -amount
305+
method = "subtract" if method == "add" else "add"
306+
op = operator.ge if op is operator.le else operator.le
307+
297308
start, end = self.start, self.end
298309

299310
i = amount

‎tests/interval/test_range.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import pytest
4+
35
import pendulum
46

57
from pendulum.interval import Interval
@@ -117,3 +119,23 @@ def test_range_amount():
117119
assert_datetime(r[1], 2016, 10, 16, 1, 0, 0)
118120
assert_datetime(r[2], 2016, 10, 18, 0, 0, 0)
119121
assert_datetime(r[3], 2016, 10, 20, 0, 0, 0)
122+
123+
124+
def test_range_zero_amount_raises():
125+
dt1 = pendulum.datetime(2000, 1, 1)
126+
dt2 = pendulum.datetime(2000, 1, 10)
127+
p = pendulum.interval(dt1, dt2)
128+
129+
with pytest.raises(ValueError, match="non-zero amount"):
130+
list(p.range("days", 0))
131+
132+
133+
def test_range_wrong_sign_amount_is_empty():
134+
dt1 = pendulum.datetime(2000, 1, 1)
135+
dt2 = pendulum.datetime(2000, 1, 10)
136+
137+
forward = pendulum.interval(dt1, dt2)
138+
assert list(forward.range("days", -1)) == []
139+
140+
inverted = pendulum.interval(dt2, dt1)
141+
assert list(inverted.range("days", -1)) == []

0 commit comments

Comments
 (0)