Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 29 additions & 82 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,14 +663,6 @@ class timedelta:

def __new__(cls, days=0, seconds=0, microseconds=0,
milliseconds=0, minutes=0, hours=0, weeks=0):
# Doing this efficiently and accurately in C is going to be difficult
# and error-prone, due to ubiquitous overflow possibilities, and that
# C double doesn't have enough bits of precision to represent
# microseconds over 10K years faithfully. The code here tries to make
# explicit where go-fast assumptions can be relied on, in order to
# guide the C implementation; it's way more convoluted than speed-
# ignoring auto-overflow-to-long idiomatic Python could be.

for name, value in (
("days", days),
("seconds", seconds),
Expand All @@ -685,80 +677,35 @@ def __new__(cls, days=0, seconds=0, microseconds=0,
f"unsupported type for timedelta {name} component: {type(value).__name__}"
)

# Final values, all integer.
# s and us fit in 32-bit signed ints; d isn't bounded.
d = s = us = 0

# Normalize everything to days, seconds, microseconds.
days += weeks*7
seconds += minutes*60 + hours*3600
microseconds += milliseconds*1000

# Get rid of all fractions, and normalize s and us.
# Take a deep breath <wink>.
if isinstance(days, float):
dayfrac, days = _math.modf(days)
daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.))
assert daysecondswhole == int(daysecondswhole) # can't overflow
s = int(daysecondswhole)
assert days == int(days)
d = int(days)
else:
daysecondsfrac = 0.0
d = days
assert isinstance(daysecondsfrac, float)
assert abs(daysecondsfrac) <= 1.0
assert isinstance(d, int)
assert abs(s) <= 24 * 3600
# days isn't referenced again before redefinition

if isinstance(seconds, float):
secondsfrac, seconds = _math.modf(seconds)
assert seconds == int(seconds)
seconds = int(seconds)
secondsfrac += daysecondsfrac
assert abs(secondsfrac) <= 2.0
else:
secondsfrac = daysecondsfrac
# daysecondsfrac isn't referenced again
assert isinstance(secondsfrac, float)
assert abs(secondsfrac) <= 2.0

assert isinstance(seconds, int)
days, seconds = divmod(seconds, 24*3600)
d += days
s += int(seconds) # can't overflow
assert isinstance(s, int)
assert abs(s) <= 2 * 24 * 3600
# seconds isn't referenced again before redefinition

usdouble = secondsfrac * 1e6
assert abs(usdouble) < 2.1e6 # exact value not critical
# secondsfrac isn't referenced again

if isinstance(microseconds, float):
microseconds = round(microseconds + usdouble)
seconds, microseconds = divmod(microseconds, 1000000)
days, seconds = divmod(seconds, 24*3600)
d += days
s += seconds
else:
microseconds = int(microseconds)
seconds, microseconds = divmod(microseconds, 1000000)
days, seconds = divmod(seconds, 24*3600)
d += days
s += seconds
microseconds = round(microseconds + usdouble)
assert isinstance(s, int)
assert isinstance(microseconds, int)
assert abs(s) <= 3 * 24 * 3600
assert abs(microseconds) < 3.1e6

# Just a little bit of carrying possible for microseconds and seconds.
seconds, us = divmod(microseconds, 1000000)
s += seconds
days, s = divmod(s, 24*3600)
d += days
# Sum the exact number of microseconds, like the C implementation:
# the integer part of each float is converted exactly, and only the
# fractional microseconds left over are added up as a float and
# rounded (half to even) at the end.
us = 0
leftover = 0.0
for value, factor in (
(microseconds, 1),
(milliseconds, 1000),
(seconds, 1000000),
(minutes, 60*1000000),
(hours, 3600*1000000),
(days, 24*3600*1000000),
(weeks, 7*24*3600*1000000)
):
if isinstance(value, float):
frac, whole = _math.modf(value)
us += int(whole) * factor
frac, whole = _math.modf(frac * factor)
us += int(whole)
leftover += frac
else:
us += int(value) * factor
if leftover:
n, m = leftover.as_integer_ratio()
us = _divide_and_round(us * m + n, m)

s, us = divmod(us, 1000000)
d, s = divmod(s, 24*3600)

assert isinstance(d, int)
assert isinstance(s, int) and 0 <= s < 24*3600
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,13 @@ def test_constructor(self):
eq(td(seconds=0.001), td(milliseconds=1))
eq(td(milliseconds=0.001), td(microseconds=1))

# Large float args keep their sub-second part
eq(td(weeks=12345678.9), td(86419752, 25920, 225))
eq(td(hours=123456789.1), td(5144032, 75959, 999979))
eq(td(minutes=1234567890.1), td(857338, 70205, 999994))
eq(td(days=1.1, weeks=1000000.1), td(7000001, 69119, 999986))
eq(td(seconds=0.5, microseconds=2.0**60), td(13343998, 77407, 346976))

# Check type of args to constructor
ra(TypeError, lambda: td(weeks='1'))
ra(TypeError, lambda: td(days='1'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix the pure-Python implementation of :class:`datetime.timedelta` losing
sub-second precision with large float arguments. It now gives the same
results as the C implementation.
Loading