Skip to content

Commit fa6a987

Browse files
committed
fix: preserve microsecond precision in Duration for large values
Signed-off-by: SAY-5 <say.apm35@gmail.com>
1 parent b99bd14 commit fa6a987

2 files changed

Lines changed: 32 additions & 10 deletions

File tree

src/pendulum/duration.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,24 @@ def __new__(
9595
)
9696

9797
# Intuitive normalization
98-
total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY
98+
# Use exact integer microseconds to avoid total_seconds() float precision loss.
99+
total_microseconds = (
100+
(timedelta.days.__get__(self) - (years * 365 + months * 30))
101+
* SECONDS_PER_DAY
102+
+ timedelta.seconds.__get__(self)
103+
) * US_PER_SECOND + timedelta.microseconds.__get__(self)
104+
total = total_microseconds / US_PER_SECOND
99105
self._total = total
100106

101107
m = 1
102-
if total < 0:
108+
if total_microseconds < 0:
103109
m = -1
104110

105-
self._microseconds = round(total % m * 1e6)
106-
self._seconds = abs(int(total)) % SECONDS_PER_DAY * m
111+
abs_microseconds = abs(total_microseconds)
112+
self._microseconds = abs_microseconds % US_PER_SECOND * m
113+
self._seconds = abs_microseconds // US_PER_SECOND % SECONDS_PER_DAY * m
107114

108-
_days = abs(int(total)) // SECONDS_PER_DAY * m
115+
_days = abs_microseconds // US_PER_SECOND // SECONDS_PER_DAY * m
109116
self._days = _days
110117
self._remaining_days = abs(_days) % 7 * m
111118
self._weeks = abs(_days) // 7 * m
@@ -509,11 +516,15 @@ def __new__(
509516
)
510517

511518
# Intuitive normalization
512-
self._total = delta.total_seconds()
513-
total = abs(self._total)
514-
515-
self._microseconds = round(total % 1 * 1e6)
516-
days, self._seconds = divmod(int(total), SECONDS_PER_DAY)
519+
# Use exact integer microseconds to avoid total_seconds() float precision loss.
520+
total_microseconds = (
521+
delta.days * SECONDS_PER_DAY + delta.seconds
522+
) * US_PER_SECOND + delta.microseconds
523+
self._total = total_microseconds / US_PER_SECOND
524+
abs_microseconds = abs(total_microseconds)
525+
526+
self._microseconds = abs_microseconds % US_PER_SECOND
527+
days, self._seconds = divmod(abs_microseconds // US_PER_SECOND, SECONDS_PER_DAY)
517528
self._days = abs(days + years * 365 + months * 30)
518529
self._weeks, self._remaining_days = divmod(days, 7)
519530
self._months = abs(months)

tests/duration/test_construct.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,14 @@ def test_float_years_and_months():
9797

9898
with pytest.raises(ValueError):
9999
pendulum.duration(months=1.5)
100+
101+
102+
def test_large_microseconds_keep_precision():
103+
micro = 8999999999999999
104+
pi = pendulum.duration(microseconds=micro)
105+
expected = timedelta(microseconds=micro)
106+
assert pi.microseconds == expected.microseconds
107+
assert pi.microseconds == 999999
108+
109+
ai = AbsoluteDuration(microseconds=micro)
110+
assert ai.microseconds == expected.microseconds == 999999

0 commit comments

Comments
 (0)