Skip to content

Commit 13800d4

Browse files
committed
Fix arithmetic with standard timedelta operands
1 parent d3f44aa commit 13800d4

3 files changed

Lines changed: 65 additions & 11 deletions

File tree

‎src/pendulum/duration.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from datetime import timedelta
44
from typing import TYPE_CHECKING
5-
from typing import cast
65
from typing import overload
76

87
import pendulum
@@ -356,6 +355,14 @@ def __neg__(self) -> Self:
356355
def _to_microseconds(self) -> int:
357356
return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
358357

358+
@staticmethod
359+
def _timedelta_microseconds(delta: timedelta) -> int:
360+
if isinstance(delta, Duration):
361+
return delta._to_microseconds()
362+
return (
363+
delta.days * SECONDS_PER_DAY + delta.seconds
364+
) * US_PER_SECOND + delta.microseconds
365+
359366
def __mul__(self, other: int | float) -> Self:
360367
if isinstance(other, int):
361368
return self.__class__(
@@ -386,10 +393,7 @@ def __floordiv__(self, other: int | timedelta) -> int | Duration:
386393

387394
usec = self._to_microseconds()
388395
if isinstance(other, timedelta):
389-
return cast(
390-
"int",
391-
usec // other._to_microseconds(), # type: ignore[attr-defined]
392-
)
396+
return usec // self._timedelta_microseconds(other)
393397

394398
if isinstance(other, int):
395399
return self.__class__(
@@ -412,10 +416,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float:
412416

413417
usec = self._to_microseconds()
414418
if isinstance(other, timedelta):
415-
return cast(
416-
"float",
417-
usec / other._to_microseconds(), # type: ignore[attr-defined]
418-
)
419+
return usec / self._timedelta_microseconds(other)
419420

420421
if isinstance(other, int):
421422
return self.__class__(
@@ -441,7 +442,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float:
441442

442443
def __mod__(self, other: timedelta) -> Self:
443444
if isinstance(other, timedelta):
444-
r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined]
445+
r = self._to_microseconds() % self._timedelta_microseconds(other)
445446

446447
return self.__class__(0, 0, r)
447448

@@ -451,7 +452,7 @@ def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
451452
if isinstance(other, timedelta):
452453
q, r = divmod(
453454
self._to_microseconds(),
454-
other._to_microseconds(), # type: ignore[attr-defined]
455+
self._timedelta_microseconds(other),
455456
)
456457

457458
return q, self.__class__(0, 0, r)

‎tests/duration/test_arithmetic.py‎

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

3+
import operator
4+
5+
from datetime import timedelta
6+
from typing import TYPE_CHECKING
7+
8+
import pytest
9+
310
import pendulum
411

512
from tests.conftest import assert_duration
613

714

15+
if TYPE_CHECKING:
16+
from collections.abc import Callable
17+
18+
19+
@pytest.mark.parametrize(
20+
"operation", [operator.truediv, operator.floordiv, operator.mod, divmod]
21+
)
22+
@pytest.mark.parametrize(
23+
"divisor", [timedelta(days=1), timedelta(seconds=-2), timedelta(microseconds=3)]
24+
)
25+
def test_arithmetic_with_standard_timedelta(
26+
operation: Callable[[timedelta, timedelta], object], divisor: timedelta
27+
) -> None:
28+
expected = timedelta(days=2, seconds=35, microseconds=522222)
29+
duration = pendulum.duration(days=2, seconds=35, microseconds=522222)
30+
assert operation(duration, divisor) == operation(expected, divisor)
31+
32+
33+
@pytest.mark.parametrize(
34+
"operation", [operator.truediv, operator.floordiv, operator.mod, divmod]
35+
)
36+
def test_arithmetic_with_zero_timedelta(
37+
operation: Callable[[timedelta, timedelta], object],
38+
) -> None:
39+
with pytest.raises(ZeroDivisionError):
40+
operation(pendulum.duration(seconds=1), timedelta())
41+
42+
43+
def test_timedelta_divisor_keeps_large_microsecond_precision() -> None:
44+
divisor = timedelta(days=200000, microseconds=1)
45+
assert pendulum.duration(days=200000) // divisor == 0
46+
47+
848
def test_multiply():
949
it = pendulum.duration(days=6, seconds=34, microseconds=522222)
1050
mul = it * 2

‎tests/interval/test_arithmetic.py‎

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

3+
from datetime import timedelta
4+
35
import pendulum
46

57
from tests.conftest import assert_duration
68

79

10+
def test_arithmetic_with_standard_timedelta():
11+
start = pendulum.datetime(2019, 1, 1)
12+
interval = pendulum.interval(start, start.add(hours=25))
13+
day = timedelta(days=1)
14+
15+
assert interval / day == 25 / 24
16+
assert interval // day == 1
17+
assert interval % day == timedelta(hours=1)
18+
assert divmod(interval, day) == (1, timedelta(hours=1))
19+
20+
821
def test_multiply():
922
dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56)
1023
dt2 = dt1.add(days=6, seconds=34)

0 commit comments

Comments
 (0)