From fa6a987d692b7a678e0339afc322f8121c880e1b Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sun, 24 May 2026 02:44:43 -0700 Subject: [PATCH] fix: preserve microsecond precision in Duration for large values Signed-off-by: SAY-5 --- src/pendulum/duration.py | 31 +++++++++++++++++++++---------- tests/duration/test_construct.py | 11 +++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py index d6cc0657..99e07863 100644 --- a/src/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -95,17 +95,24 @@ def __new__( ) # Intuitive normalization - total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY + # Use exact integer microseconds to avoid total_seconds() float precision loss. + total_microseconds = ( + (timedelta.days.__get__(self) - (years * 365 + months * 30)) + * SECONDS_PER_DAY + + timedelta.seconds.__get__(self) + ) * US_PER_SECOND + timedelta.microseconds.__get__(self) + total = total_microseconds / US_PER_SECOND self._total = total m = 1 - if total < 0: + if total_microseconds < 0: m = -1 - self._microseconds = round(total % m * 1e6) - self._seconds = abs(int(total)) % SECONDS_PER_DAY * m + abs_microseconds = abs(total_microseconds) + self._microseconds = abs_microseconds % US_PER_SECOND * m + self._seconds = abs_microseconds // US_PER_SECOND % SECONDS_PER_DAY * m - _days = abs(int(total)) // SECONDS_PER_DAY * m + _days = abs_microseconds // US_PER_SECOND // SECONDS_PER_DAY * m self._days = _days self._remaining_days = abs(_days) % 7 * m self._weeks = abs(_days) // 7 * m @@ -509,11 +516,15 @@ def __new__( ) # Intuitive normalization - self._total = delta.total_seconds() - total = abs(self._total) - - self._microseconds = round(total % 1 * 1e6) - days, self._seconds = divmod(int(total), SECONDS_PER_DAY) + # Use exact integer microseconds to avoid total_seconds() float precision loss. + total_microseconds = ( + delta.days * SECONDS_PER_DAY + delta.seconds + ) * US_PER_SECOND + delta.microseconds + self._total = total_microseconds / US_PER_SECOND + abs_microseconds = abs(total_microseconds) + + self._microseconds = abs_microseconds % US_PER_SECOND + days, self._seconds = divmod(abs_microseconds // US_PER_SECOND, SECONDS_PER_DAY) self._days = abs(days + years * 365 + months * 30) self._weeks, self._remaining_days = divmod(days, 7) self._months = abs(months) diff --git a/tests/duration/test_construct.py b/tests/duration/test_construct.py index aaa53909..c369d875 100644 --- a/tests/duration/test_construct.py +++ b/tests/duration/test_construct.py @@ -97,3 +97,14 @@ def test_float_years_and_months(): with pytest.raises(ValueError): pendulum.duration(months=1.5) + + +def test_large_microseconds_keep_precision(): + micro = 8999999999999999 + pi = pendulum.duration(microseconds=micro) + expected = timedelta(microseconds=micro) + assert pi.microseconds == expected.microseconds + assert pi.microseconds == 999999 + + ai = AbsoluteDuration(microseconds=micro) + assert ai.microseconds == expected.microseconds == 999999