Skip to content

Commit dcd2558

Browse files
miss-islingtoncanblmz1StanFromIreland
authored
[3.15] gh-160806: Fix precision loss in _pydatetime.timedelta with large float arguments (GH-160807) (#160889)
(cherry picked from commit 2b6f543) Co-authored-by: canblmz1 <116688414+canblmz1@users.noreply.github.com> Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent be23450 commit dcd2558

3 files changed

Lines changed: 39 additions & 82 deletions

File tree

‎Lib/_pydatetime.py‎

Lines changed: 29 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -656,14 +656,6 @@ class timedelta:
656656

657657
def __new__(cls, days=0, seconds=0, microseconds=0,
658658
milliseconds=0, minutes=0, hours=0, weeks=0):
659-
# Doing this efficiently and accurately in C is going to be difficult
660-
# and error-prone, due to ubiquitous overflow possibilities, and that
661-
# C double doesn't have enough bits of precision to represent
662-
# microseconds over 10K years faithfully. The code here tries to make
663-
# explicit where go-fast assumptions can be relied on, in order to
664-
# guide the C implementation; it's way more convoluted than speed-
665-
# ignoring auto-overflow-to-long idiomatic Python could be.
666-
667659
for name, value in (
668660
("days", days),
669661
("seconds", seconds),
@@ -678,80 +670,35 @@ def __new__(cls, days=0, seconds=0, microseconds=0,
678670
f"unsupported type for timedelta {name} component: {type(value).__name__}"
679671
)
680672

681-
# Final values, all integer.
682-
# s and us fit in 32-bit signed ints; d isn't bounded.
683-
d = s = us = 0
684-
685-
# Normalize everything to days, seconds, microseconds.
686-
days += weeks*7
687-
seconds += minutes*60 + hours*3600
688-
microseconds += milliseconds*1000
689-
690-
# Get rid of all fractions, and normalize s and us.
691-
# Take a deep breath <wink>.
692-
if isinstance(days, float):
693-
dayfrac, days = _math.modf(days)
694-
daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.))
695-
assert daysecondswhole == int(daysecondswhole) # can't overflow
696-
s = int(daysecondswhole)
697-
assert days == int(days)
698-
d = int(days)
699-
else:
700-
daysecondsfrac = 0.0
701-
d = days
702-
assert isinstance(daysecondsfrac, float)
703-
assert abs(daysecondsfrac) <= 1.0
704-
assert isinstance(d, int)
705-
assert abs(s) <= 24 * 3600
706-
# days isn't referenced again before redefinition
707-
708-
if isinstance(seconds, float):
709-
secondsfrac, seconds = _math.modf(seconds)
710-
assert seconds == int(seconds)
711-
seconds = int(seconds)
712-
secondsfrac += daysecondsfrac
713-
assert abs(secondsfrac) <= 2.0
714-
else:
715-
secondsfrac = daysecondsfrac
716-
# daysecondsfrac isn't referenced again
717-
assert isinstance(secondsfrac, float)
718-
assert abs(secondsfrac) <= 2.0
719-
720-
assert isinstance(seconds, int)
721-
days, seconds = divmod(seconds, 24*3600)
722-
d += days
723-
s += int(seconds) # can't overflow
724-
assert isinstance(s, int)
725-
assert abs(s) <= 2 * 24 * 3600
726-
# seconds isn't referenced again before redefinition
727-
728-
usdouble = secondsfrac * 1e6
729-
assert abs(usdouble) < 2.1e6 # exact value not critical
730-
# secondsfrac isn't referenced again
731-
732-
if isinstance(microseconds, float):
733-
microseconds = round(microseconds + usdouble)
734-
seconds, microseconds = divmod(microseconds, 1000000)
735-
days, seconds = divmod(seconds, 24*3600)
736-
d += days
737-
s += seconds
738-
else:
739-
microseconds = int(microseconds)
740-
seconds, microseconds = divmod(microseconds, 1000000)
741-
days, seconds = divmod(seconds, 24*3600)
742-
d += days
743-
s += seconds
744-
microseconds = round(microseconds + usdouble)
745-
assert isinstance(s, int)
746-
assert isinstance(microseconds, int)
747-
assert abs(s) <= 3 * 24 * 3600
748-
assert abs(microseconds) < 3.1e6
749-
750-
# Just a little bit of carrying possible for microseconds and seconds.
751-
seconds, us = divmod(microseconds, 1000000)
752-
s += seconds
753-
days, s = divmod(s, 24*3600)
754-
d += days
673+
# Sum the exact number of microseconds, like the C implementation:
674+
# the integer part of each float is converted exactly, and only the
675+
# fractional microseconds left over are added up as a float and
676+
# rounded (half to even) at the end.
677+
us = 0
678+
leftover = 0.0
679+
for value, factor in (
680+
(microseconds, 1),
681+
(milliseconds, 1000),
682+
(seconds, 1000000),
683+
(minutes, 60*1000000),
684+
(hours, 3600*1000000),
685+
(days, 24*3600*1000000),
686+
(weeks, 7*24*3600*1000000)
687+
):
688+
if isinstance(value, float):
689+
frac, whole = _math.modf(value)
690+
us += int(whole) * factor
691+
frac, whole = _math.modf(frac * factor)
692+
us += int(whole)
693+
leftover += frac
694+
else:
695+
us += int(value * factor)
696+
if leftover:
697+
n, m = leftover.as_integer_ratio()
698+
us = _divide_and_round(us * m + n, m)
699+
700+
s, us = divmod(us, 1000000)
701+
d, s = divmod(s, 24*3600)
755702

756703
assert isinstance(d, int)
757704
assert isinstance(s, int) and 0 <= s < 24*3600

‎Lib/test/datetimetester.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,13 @@ def test_constructor(self):
543543
eq(td(seconds=0.001), td(milliseconds=1))
544544
eq(td(milliseconds=0.001), td(microseconds=1))
545545

546+
# Large float args keep their sub-second part
547+
eq(td(weeks=12345678.9), td(86419752, 25920, 225))
548+
eq(td(hours=123456789.1), td(5144032, 75959, 999979))
549+
eq(td(minutes=1234567890.1), td(857338, 70205, 999994))
550+
eq(td(days=1.1, weeks=1000000.1), td(7000001, 69119, 999986))
551+
eq(td(seconds=0.5, microseconds=2.0**60), td(13343998, 77407, 346976))
552+
546553
# Check type of args to constructor
547554
ra(TypeError, lambda: td(weeks='1'))
548555
ra(TypeError, lambda: td(days='1'))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the pure-Python implementation of :class:`datetime.timedelta` losing
2+
sub-second precision with large float arguments. It now gives the same
3+
results as the C implementation.

0 commit comments

Comments
 (0)