From 25be42df1f606c087d2b7f644b7856d41fac352d Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 29 Jun 2026 21:17:00 +0200 Subject: [PATCH] Fix Duration multiplication by a float dropping years and months Duration.__mul__ with a float built the result only from _to_microseconds(), which excludes the years and months components, so Duration(years=1) * 2.0 returned an empty Duration even though Duration(years=1) * 2 (int) and Duration(years=1) / 2.0 (float) both keep them. Scale years and months by the float ratio as well, mirroring the existing __truediv__ implementation. --- src/pendulum/duration.py | 8 +++++++- tests/duration/test_arithmetic.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py index d6cc0657..6337d550 100644 --- a/src/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -368,7 +368,13 @@ def __mul__(self, other: int | float) -> Self: usec = self._to_microseconds() a, b = other.as_integer_ratio() - return self.__class__(0, 0, _divide_and_round(usec * a, b)) + return self.__class__( + 0, + 0, + _divide_and_round(usec * a, b), + years=_divide_and_round(self._years * a, b), + months=_divide_and_round(self._months * a, b), + ) return NotImplemented diff --git a/tests/duration/test_arithmetic.py b/tests/duration/test_arithmetic.py index cba4d39d..f79370fb 100644 --- a/tests/duration/test_arithmetic.py +++ b/tests/duration/test_arithmetic.py @@ -27,6 +27,40 @@ def test_multiply(): assert_duration(mul, 4, 6, 9, 5, 0, 1, 9, 44444) +def test_multiply_float(): + # A float factor must scale years and months just like an int factor (and + # like float division already does). Regression: Duration.__mul__ with a + # float dropped the years/months components entirely. + it = pendulum.duration( + years=2, months=3, weeks=4, days=6, seconds=34, microseconds=522222 + ) + mul = it * 2.0 + + assert isinstance(mul, pendulum.Duration) + assert_duration(mul, 4, 6, 9, 5, 0, 1, 9, 44444) + + mul = 2.0 * it + + assert isinstance(mul, pendulum.Duration) + assert_duration(mul, 4, 6, 9, 5, 0, 1, 9, 44444) + + # A whole-valued float must match the integer result exactly. + it = pendulum.duration(years=1, months=6, days=2, seconds=35, microseconds=522222) + by_int = it * 3 + by_float = it * 3.0 + + assert (by_float.years, by_float.months) == (by_int.years, by_int.months) + assert by_float.total_seconds() == by_int.total_seconds() + + # A non-integer factor rounds years/months, mirroring float division. + it = pendulum.duration(years=2, months=4, days=2, seconds=35, microseconds=522222) + mul = it * 1.5 + + assert isinstance(mul, pendulum.Duration) + assert mul.years == 3 + assert mul.months == 6 + + def test_divide(): it = pendulum.duration(days=2, seconds=34, microseconds=522222) mul = it / 2