Restoring division in Python
Binary long division that restores the partial remainder after an unsuccessful subtraction.
- File
restoring_division.py, 129 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/restoring-division/. The authors are listed in prov/7J9632.jsonl.
"""Restoring binary division on limb arrays.
Numbers are lists of limbs in radix 2**w, least significant limb first. The
partial remainder R is kept in n + 1 limbs, where n is the number of limbs of
the divisor, and a negative value of R is detected by the borrow out of the
trial subtraction, as a hardware divider does.
"""
def _trim(a: list[int]) -> list[int]:
while a and a[-1] == 0:
a.pop()
return a
def _shift_in(r: list[int], bit: int, w: int) -> None:
"""r <- 2 r + bit, in place. The top bit shifted out is dropped."""
mask = (1 << w) - 1
carry = bit
for i in range(len(r)):
t = (r[i] << 1) | carry
carry = t >> w
r[i] = t & mask
def _sub_in_place(r: list[int], v: list[int], w: int) -> int:
"""r <- r - v modulo 2**(w len(r)); returns the borrow out (1 if r < v)."""
mask = (1 << w) - 1
borrow = 0
for i in range(len(r)):
t = r[i] - (v[i] if i < len(v) else 0) - borrow
borrow = 1 if t < 0 else 0
r[i] = t & mask
return borrow
def _add_in_place(r: list[int], v: list[int], w: int) -> None:
"""r <- r + v modulo 2**(w len(r)); the carry out is dropped."""
mask = (1 << w) - 1
carry = 0
for i in range(len(r)):
t = r[i] + (v[i] if i < len(v) else 0) + carry
carry = t >> w
r[i] = t & mask
def bit_length(u: list[int], w: int) -> int:
u = _trim(list(u))
if not u:
return 0
return (len(u) - 1) * w + u[-1].bit_length()
def restoring_divide(u: list[int], v: list[int], w: int = 8, stats: dict | None = None) -> tuple[list[int], list[int]]:
"""Quotient and remainder of the limb arrays u and v (v nonzero) in radix 2**w.
One quotient bit per step, from the top: shift the next bit of u into the
partial remainder, subtract v, and add v back if the result is negative.
If stats is given, it counts the steps and the restoring additions.
"""
v = _trim(list(v))
if not v:
raise ZeroDivisionError("division by zero")
n = len(v)
m = bit_length(u, w)
q = [0] * ((m + w - 1) // w)
r = [0] * (n + 1) # @line L1: R <- 0, Q <- 0
restores = 0
for i in range(m - 1, -1, -1): # @line L2
bit = (u[i // w] >> (i % w)) & 1
_shift_in(r, bit, w) # @line L3: R <- 2R + u_i
negative = _sub_in_place(r, v, w) # @line L4: trial subtraction
if negative: # @line L5
_add_in_place(r, v, w) # @line L6: restore, q_i = 0
restores += 1
else:
q[i // w] |= 1 << (i % w) # @line L7: q_i = 1
if stats is not None:
stats["steps"] = stats.get("steps", 0) + m
stats["restores"] = stats.get("restores", 0) + restores
return _trim(q), _trim(r) # @line L8
def nonperforming_divide(u: list[int], v: list[int], w: int = 8) -> tuple[list[int], list[int]]:
"""The non-performing variant: subtract into a copy and keep it only if it is nonnegative."""
v = _trim(list(v))
if not v:
raise ZeroDivisionError("division by zero")
m = bit_length(u, w)
q = [0] * ((m + w - 1) // w)
r = [0] * (len(v) + 1)
for i in range(m - 1, -1, -1):
_shift_in(r, (u[i // w] >> (i % w)) & 1, w)
t = list(r)
if not _sub_in_place(t, v, w):
r = t
q[i // w] |= 1 << (i % w)
return _trim(q), _trim(r)
def to_limbs(x: int, w: int) -> list[int]:
out = []
mask = (1 << w) - 1
while x:
out.append(x & mask)
x >>= w
return out
def from_limbs(a: list[int], w: int) -> int:
x = 0
for t in reversed(a):
x = (x << w) | t
return x
def divide_signed(a: int, b: int, w: int = 8) -> tuple[int, int]:
"""Truncating signed division, as in C: the quotient rounds toward zero and
the remainder has the sign of the dividend."""
q, r = restoring_divide(to_limbs(abs(a), w), to_limbs(abs(b), w), w)
qi, ri = from_limbs(q, w), from_limbs(r, w)
if (a < 0) != (b < 0):
qi = -qi
if a < 0:
ri = -ri
return qi, ri
test_restoring_division.py 79 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/restoring-division/. The authors are listed in prov/7J9632.jsonl.
import random
import pytest
from restoring_division import (
divide_signed,
from_limbs,
nonperforming_divide,
restoring_divide,
to_limbs,
)
def check(a: int, b: int, w: int) -> None:
q, r = restoring_divide(to_limbs(a, w), to_limbs(b, w), w)
assert (from_limbs(q, w), from_limbs(r, w)) == divmod(a, b)
assert not q or q[-1] != 0
assert not r or r[-1] != 0
q2, r2 = nonperforming_divide(to_limbs(a, w), to_limbs(b, w), w)
assert (q2, r2) == (q, r)
def test_example() -> None:
# 100 = 14 * 7 + 2: 7 steps for the 7 binary digits of 100, and 4 restores (quotient bits 0001110).
stats: dict = {}
q, r = restoring_divide([100], [7], 8, stats)
assert (q, r) == ([14], [2])
assert stats == {"steps": 7, "restores": 4}
def test_edges() -> None:
for w in (4, 8):
B = 1 << w
for a in [0, 1, 2, B - 1, B, B + 1, B * B - 1, B**3, B**4 - 1]:
for b in [1, 2, 3, B - 1, B, B + 1, B * B - 1, a if a else 1, a + 1]:
check(a, b, w)
def test_zero_divisor() -> None:
with pytest.raises(ZeroDivisionError):
restoring_divide([5], [], 8)
with pytest.raises(ZeroDivisionError):
restoring_divide([5], [0, 0], 8)
def test_leading_zero_limbs() -> None:
q, r = restoring_divide([100, 0, 0], [7, 0], 8)
assert (q, r) == ([14], [2])
def test_exhaustive_small() -> None:
for a in range(256):
for b in range(1, 40):
check(a, b, 4)
def test_random() -> None:
rng = random.Random(1946)
for w in (4, 8):
for _ in range(600):
a = rng.getrandbits(rng.randint(0, 160))
b = rng.getrandbits(rng.randint(1, 100)) or 1
check(a, b, w)
check(a, a // 3 + 1, w)
def test_signed() -> None:
rng = random.Random(7)
for _ in range(400):
a = rng.randint(-(10**12), 10**12)
b = rng.choice([-1, 1]) * rng.randint(1, 10**6)
q, r = divide_signed(a, b)
assert q * b + r == a
assert abs(r) < abs(b)
assert r == 0 or (r < 0) == (a < 0)
assert q == int(a / b) or abs(a) > 2**52
What the next level needs
- the tests did not run in this build