Algorithm Search⌘K

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
restoring_division.pyRawGitHub
# 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

Read the full page about Restoring division