Skip to content

Commit 603545a

Browse files
miss-islingtoncanblmz1StanFromIreland
authored
[3.14] gh-160806: Fix precision loss in _pydatetime.timedelta with large float arguments (GH-160807) (#160890)
(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 872e4c7 commit 603545a

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
@@ -647,14 +647,6 @@ class timedelta:
647647

648648
def __new__(cls, days=0, seconds=0, microseconds=0,
649649
milliseconds=0, minutes=0, hours=0, weeks=0):
650-
# Doing this efficiently and accurately in C is going to be difficult
651-
# and error-prone, due to ubiquitous overflow possibilities, and that
652-
# C double doesn't have enough bits of precision to represent
653-
# microseconds over 10K years faithfully. The code here tries to make
654-
# explicit where go-fast assumptions can be relied on, in order to
655-
# guide the C implementation; it's way more convoluted than speed-
656-
# ignoring auto-overflow-to-long idiomatic Python could be.
657-
658650
for name, value in (
659651
("days", days),
660652
("seconds", seconds),
@@ -669,80 +661,35 @@ def __new__(cls, days=0, seconds=0, microseconds=0,
669661
f"unsupported type for timedelta {name} component: {type(value).__name__}"
670662
)
671663

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

747694
assert isinstance(d, int)
748695
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
@@ -542,6 +542,13 @@ def test_constructor(self):
542542
eq(td(seconds=0.001), td(milliseconds=1))
543543
eq(td(milliseconds=0.001), td(microseconds=1))
544544

545+
# Large float args keep their sub-second part
546+
eq(td(weeks=12345678.9), td(86419752, 25920, 225))
547+
eq(td(hours=123456789.1), td(5144032, 75959, 999979))
548+
eq(td(minutes=1234567890.1), td(857338, 70205, 999994))
549+
eq(td(days=1.1, weeks=1000000.1), td(7000001, 69119, 999986))
550+
eq(td(seconds=0.5, microseconds=2.0**60), td(13343998, 77407, 346976))
551+
545552
# Check type of args to constructor
546553
ra(TypeError, lambda: td(weeks='1'))
547554
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)