Long division in Python
Digit-by-digit division that estimates each quotient digit and subtracts.
- File
long_division.py, 120 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/long-division/. The authors are listed in prov/BHDBGW.jsonl.
def _normalize(a: list[int]) -> list[int]:
while a and a[-1] == 0:
a.pop()
return a
def short_divide(u: list[int], v: int, base: int = 256) -> tuple[list[int], int]:
"""Divide the digit array u (least significant first) by one digit 0 < v < base.
Returns (quotient digits, remainder). Each step divides a two-digit number
r * base + u[j] by v, where r < v, so each quotient digit is below base.
"""
if not 0 < v < base:
raise ValueError("the divisor must be one nonzero digit")
q = [0] * len(u)
r = 0
for j in range(len(u) - 1, -1, -1):
t = r * base + u[j]
q[j], r = divmod(t, v)
return _normalize(q), r
def long_divide(u: list[int], v: list[int], base: int = 256, trace: list | None = None) -> tuple[list[int], list[int]]:
"""Knuth's Algorithm D: quotient and remainder of digit arrays in radix base.
u and v are least significant digit first; v must be nonzero. The result
arrays are normalized (no leading zero digits; zero is []). Cost:
Theta(n (m + 1)) digit operations for n = len(v) and m = len(u) - n.
"""
u = _normalize(list(u))
v = _normalize(list(v))
if not v:
raise ZeroDivisionError("division by zero")
n = len(v)
if len(u) < n:
return [], u
if n == 1:
q, r = short_divide(u, v[0], base)
return q, ([r] if r else [])
m = len(u) - n
d = base // (v[-1] + 1) # @line L1: normalize, so that v[n-1] >= base // 2
u = _mul_digit(u, d, base) + [0]
u = u[: m + n + 1] + [0] * (m + n + 1 - len(u))
v = _mul_digit(v, d, base)[:n]
q = [0] * (m + 1)
for j in range(m, -1, -1): # @line L2
top = u[j + n] * base + u[j + n - 1]
qhat, rhat = divmod(top, v[n - 1]) # @line L3: estimate from the leading digits
while rhat < base and (qhat >= base or qhat * v[n - 2] > rhat * base + u[j + n - 2]): # @line L4
qhat -= 1
rhat += v[n - 1]
borrow = 0 # @line L5: u[j .. j + n] <- u[j .. j + n] - qhat * v
carry = 0
for i in range(n + 1):
p = qhat * (v[i] if i < n else 0) + carry
carry = p // base
t = u[j + i] - (p % base) - borrow
borrow = 1 if t < 0 else 0
u[j + i] = t + base * borrow
added_back = False
if borrow: # @line L6: qhat was one too large, add v back once
qhat -= 1
added_back = True
c = 0
for i in range(n + 1):
t = u[j + i] + (v[i] if i < n else 0) + c
u[j + i] = t % base
c = t // base
q[j] = qhat # @line L7
if trace is not None:
trace.append((j, divmod(top, v[n - 1])[0], qhat, added_back))
r, rest = short_divide(u[:n], d, base) # @line L8: undo the normalization
assert rest == 0
return _normalize(q), r # @line L9
def _mul_digit(a: list[int], d: int, base: int) -> list[int]:
out = []
c = 0
for x in a:
t = x * d + c
out.append(t % base)
c = t // base
out.append(c)
return out
def divmod_signed(a: int, b: int, base: int = 256) -> tuple[int, int]:
"""Floor division of Python ints through long_divide on their magnitudes.
Returns (q, r) with a = q b + r and 0 <= r < |b| when b > 0, or -|b| < r <= 0 when b < 0,
matching Python's divmod.
"""
def to_digits(x: int) -> list[int]:
out = []
while x:
x, t = divmod(x, base)
out.append(t)
return out
def val(d: list[int]) -> int:
x = 0
for t in reversed(d):
x = x * base + t
return x
qd, rd = long_divide(to_digits(abs(a)), to_digits(abs(b)), base)
q, r = val(qd), val(rd)
if (a < 0) != (b < 0):
q = -q
if r:
q -= 1
r = abs(b) - r
if b < 0:
r = -r
return q, r
test_long_division.py 100 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/long-division/. The authors are listed in prov/BHDBGW.jsonl.
import random
import pytest
from long_division import divmod_signed, long_divide, short_divide
def digits(x: int, base: int) -> list[int]:
out = []
while x:
x, t = divmod(x, base)
out.append(t)
return out
def value(d: list[int], base: int) -> int:
x = 0
for t in reversed(d):
x = x * base + t
return x
def check(a: int, b: int, base: int) -> None:
q, r = long_divide(digits(a, base), digits(b, base), base)
assert (value(q, base), value(r, base)) == divmod(a, b)
assert q == digits(a // b, base) and r == digits(a % b, base)
def test_examples() -> None:
trace: list = []
q, r = long_divide(digits(814600, 10), digits(199, 10), 10, trace)
assert value(q, 10) == 4093 and value(r, 10) == 93
assert [t[2] for t in trace] == [4, 0, 9, 3]
trace = []
q, r = long_divide(digits(4100, 10), digits(588, 10), 10, trace)
assert value(q, 10) == 6 and value(r, 10) == 572
assert trace[-1] == (0, 8, 6, True)
def test_short_division() -> None:
assert short_divide([13, 15, 15], 7, 16) == (digits(4093 // 7, 16), 4093 % 7)
with pytest.raises(ValueError):
short_divide([1], 0, 16)
def test_edges() -> None:
for base in (16, 256):
top = base - 1
cases = [
(0, 1), (1, 1), (0, 12345), (5, 7), (top, top), (top, 1), (base, top), (base**4, 1),
(base**6 - 1, base**3 - 1), (base**6 - 1, base**6 - 1), (base**6, base**6 - 1),
(base**8 - 1, base // 2), (base**8 - 1, base**2 // 2), (base**9 + 5, base**8 + 1),
(base**10 - 1, top), (base**7, base**3 + 1), (base**12 - 1, 3 * base**5 + 7),
]
for a, b in cases:
check(a, b, base)
with pytest.raises(ZeroDivisionError):
long_divide([1, 2], [], 16)
with pytest.raises(ZeroDivisionError):
long_divide([1, 2], [0, 0], 16)
def test_random() -> None:
rng = random.Random(1969)
for base in (16, 256):
for _ in range(2500):
nb = rng.randint(1, 12)
na = rng.randint(0, 24)
b = rng.getrandbits(nb * (base.bit_length() - 1)) or 1
a = rng.getrandbits(na * (base.bit_length() - 1))
if rng.random() < 0.2:
b = (b | (base - 1)) if rng.random() < 0.5 else b | (1 << (nb * (base.bit_length() - 1) - 1))
a = base ** (na + 1) - 1
check(a, b, base)
def test_add_back_happens() -> None:
rng = random.Random(7)
seen = 0
for _ in range(3000):
b = rng.randint(17, 16**4)
a = rng.randint(0, 16**9)
trace: list = []
q, r = long_divide(digits(a, 16), digits(b, 16), 16, trace)
assert (value(q, 16), value(r, 16)) == divmod(a, b)
seen += sum(1 for t in trace if t[3])
assert seen > 0
def test_signed() -> None:
rng = random.Random(11)
for a, b in [(7, 2), (-7, 2), (7, -2), (-7, -2), (0, -3), (-6, 3), (6, -3)]:
assert divmod_signed(a, b, 16) == divmod(a, b)
for _ in range(1500):
a = rng.randint(-(16**10), 16**10)
b = rng.randint(-(16**5), 16**5) or 1
assert divmod_signed(a, b, 16) == divmod(a, b)
What the next level needs
- the tests did not run in this build