diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py index d6cc0657d..08066b358 100644 --- a/src/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -2,7 +2,6 @@ from datetime import timedelta from typing import TYPE_CHECKING -from typing import cast from typing import overload import pendulum @@ -356,6 +355,14 @@ def __neg__(self) -> Self: def _to_microseconds(self) -> int: return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds + @staticmethod + def _timedelta_microseconds(delta: timedelta) -> int: + if isinstance(delta, Duration): + return delta._to_microseconds() + return ( + delta.days * SECONDS_PER_DAY + delta.seconds + ) * US_PER_SECOND + delta.microseconds + def __mul__(self, other: int | float) -> Self: if isinstance(other, int): return self.__class__( @@ -386,10 +393,7 @@ def __floordiv__(self, other: int | timedelta) -> int | Duration: usec = self._to_microseconds() if isinstance(other, timedelta): - return cast( - "int", - usec // other._to_microseconds(), # type: ignore[attr-defined] - ) + return usec // self._timedelta_microseconds(other) if isinstance(other, int): return self.__class__( @@ -412,10 +416,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float: usec = self._to_microseconds() if isinstance(other, timedelta): - return cast( - "float", - usec / other._to_microseconds(), # type: ignore[attr-defined] - ) + return usec / self._timedelta_microseconds(other) if isinstance(other, int): return self.__class__( @@ -441,7 +442,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float: def __mod__(self, other: timedelta) -> Self: if isinstance(other, timedelta): - r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] + r = self._to_microseconds() % self._timedelta_microseconds(other) return self.__class__(0, 0, r) @@ -451,7 +452,7 @@ def __divmod__(self, other: timedelta) -> tuple[int, Duration]: if isinstance(other, timedelta): q, r = divmod( self._to_microseconds(), - other._to_microseconds(), # type: ignore[attr-defined] + self._timedelta_microseconds(other), ) return q, self.__class__(0, 0, r) diff --git a/tests/duration/test_arithmetic.py b/tests/duration/test_arithmetic.py index cba4d39d0..d8409def8 100644 --- a/tests/duration/test_arithmetic.py +++ b/tests/duration/test_arithmetic.py @@ -1,10 +1,50 @@ from __future__ import annotations +import operator + +from datetime import timedelta +from typing import TYPE_CHECKING + +import pytest + import pendulum from tests.conftest import assert_duration +if TYPE_CHECKING: + from collections.abc import Callable + + +@pytest.mark.parametrize( + "operation", [operator.truediv, operator.floordiv, operator.mod, divmod] +) +@pytest.mark.parametrize( + "divisor", [timedelta(days=1), timedelta(seconds=-2), timedelta(microseconds=3)] +) +def test_arithmetic_with_standard_timedelta( + operation: Callable[[timedelta, timedelta], object], divisor: timedelta +) -> None: + expected = timedelta(days=2, seconds=35, microseconds=522222) + duration = pendulum.duration(days=2, seconds=35, microseconds=522222) + assert operation(duration, divisor) == operation(expected, divisor) + + +@pytest.mark.parametrize( + "operation", [operator.truediv, operator.floordiv, operator.mod, divmod] +) +def test_arithmetic_with_zero_timedelta( + operation: Callable[[timedelta, timedelta], object], +) -> None: + with pytest.raises(ZeroDivisionError): + operation(pendulum.duration(seconds=1), timedelta()) + + +def test_timedelta_divisor_keeps_large_microsecond_precision() -> None: + divisor = timedelta(days=200000, microseconds=1) + assert pendulum.duration(days=200000) // divisor == 0 + + def test_multiply(): it = pendulum.duration(days=6, seconds=34, microseconds=522222) mul = it * 2 diff --git a/tests/interval/test_arithmetic.py b/tests/interval/test_arithmetic.py index e5ba01f4e..15f014379 100644 --- a/tests/interval/test_arithmetic.py +++ b/tests/interval/test_arithmetic.py @@ -1,10 +1,23 @@ from __future__ import annotations +from datetime import timedelta + import pendulum from tests.conftest import assert_duration +def test_arithmetic_with_standard_timedelta(): + start = pendulum.datetime(2019, 1, 1) + interval = pendulum.interval(start, start.add(hours=25)) + day = timedelta(days=1) + + assert interval / day == 25 / 24 + assert interval // day == 1 + assert interval % day == timedelta(hours=1) + assert divmod(interval, day) == (1, timedelta(hours=1)) + + def test_multiply(): dt1 = pendulum.DateTime(2016, 8, 7, 12, 34, 56) dt2 = dt1.add(days=6, seconds=34)