Algorithm Search⌘K

Multiprecision integer representation in Python

Stores an integer as an array of fixed-size digits (limbs) with a sign.

File
multiprecision_integer.py, 183 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
multiprecision_integer.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/multiprecision-integer/. The authors are listed in prov/WZN2G1.jsonl.

from __future__ import annotations


class MultiprecisionInteger:
    """A signed integer stored as a sign and an array of limbs in base 2^w.

    The limbs are least significant first. Invariant: the top limb is not zero,
    the value 0 has no limbs, and 0 is never negative. Costs are in limb
    operations: add and sub take O(m + n), mul takes O(m n), divmod_limb O(n).
    """

    def __init__(self, w: int = 8) -> None:
        """The value 0 in base 2^w, for w >= 1."""
        if w < 1:
            raise ValueError("the limb width w must be at least 1")
        self.w = w
        self.base = 1 << w
        self.neg = False
        self.limbs: list[int] = []

    # ---------------------------------------------------------------- conversion

    @classmethod
    def from_int(cls, x: int, w: int = 8) -> MultiprecisionInteger:
        """The value x in base 2^w. Cost O(n) limb operations for n limbs."""
        r = cls(w)
        r.neg = x < 0
        m = -x if x < 0 else x
        while m:
            r.limbs.append(m & (r.base - 1))
            m >>= w
        return r

    @classmethod
    def from_limbs(cls, neg: bool, limbs: list[int], w: int = 8) -> MultiprecisionInteger:
        """The value (-1)^neg * sum limbs[i] * 2^(w i), normalized."""
        r = cls(w)
        if any(not 0 <= d < r.base for d in limbs):
            raise ValueError("each limb must be in [0, 2^w)")
        r.limbs = _normalize(list(limbs))
        r.neg = neg and bool(r.limbs)
        return r

    def to_int(self) -> int:
        """The value as a Python int, by Horner's rule from the top limb."""
        v = 0
        for d in reversed(self.limbs):
            v = (v << self.w) | d
        return -v if self.neg else v

    def _new(self, neg: bool, limbs: list[int]) -> MultiprecisionInteger:
        r = MultiprecisionInteger(self.w)
        r.limbs = limbs
        r.neg = neg and bool(limbs)
        return r

    def _same_base(self, y: MultiprecisionInteger) -> None:
        if self.w != y.w:
            raise ValueError("both operands need the same limb width")

    # ---------------------------------------------------------------- operations

    def neg_(self) -> MultiprecisionInteger:
        """-x. Cost O(n) for the copy."""
        return self._new(not self.neg, list(self.limbs))

    def compare(self, y: MultiprecisionInteger) -> int:
        """The sign of x - y: -1, 0 or 1. Cost O(n), O(1) if the signs or lengths differ."""
        self._same_base(y)
        if self.neg != y.neg:
            return -1 if self.neg else 1
        c = _cmp_mag(self.limbs, y.limbs)
        return -c if self.neg else c

    def add(self, y: MultiprecisionInteger) -> MultiprecisionInteger:
        """x + y. Cost O(m + n) limb operations."""
        self._same_base(y)
        if self.neg == y.neg:  # @line L1
            return self._new(self.neg, _add_mag(self.limbs, y.limbs, self.base))  # @line L2
        c = _cmp_mag(self.limbs, y.limbs)  # @line L3
        if c == 0:
            return MultiprecisionInteger(self.w)  # @line L4: zero is never negative
        if c > 0:  # @line L5
            return self._new(self.neg, _sub_mag(self.limbs, y.limbs, self.base))
        return self._new(y.neg, _sub_mag(y.limbs, self.limbs, self.base))  # @line L6

    def sub(self, y: MultiprecisionInteger) -> MultiprecisionInteger:
        """x - y, as x + (-y). Cost O(m + n)."""
        return self.add(y.neg_())

    def mul(self, y: MultiprecisionInteger) -> MultiprecisionInteger:
        """x * y by the schoolbook method. Cost O(m n) limb operations."""
        self._same_base(y)
        a, b, base, w = self.limbs, y.limbs, self.base, self.w
        if not a or not b:
            return MultiprecisionInteger(self.w)
        c = [0] * (len(a) + len(b))
        for i, ai in enumerate(a):
            carry = 0
            for j, bj in enumerate(b):
                t = c[i + j] + ai * bj + carry  # at most (base - 1) * (base + 1) < base^2
                c[i + j] = t & (base - 1)
                carry = t >> w
            c[i + len(b)] = carry
        return self._new(self.neg != y.neg, _normalize(c))

    def divmod_limb(self, d: int) -> tuple[MultiprecisionInteger, int]:
        """(q, r) with |x| = q d + r and 0 <= r < d, for one limb 0 < d < 2^w.

        The quotient gets the sign of x, so q truncates toward zero. Cost O(n).
        """
        if not 0 < d < self.base:
            raise ValueError("the divisor must be one nonzero limb")
        q = [0] * len(self.limbs)
        r = 0
        for i in range(len(self.limbs) - 1, -1, -1):
            t = (r << self.w) | self.limbs[i]  # t < d * base
            q[i], r = t // d, t % d
        return self._new(self.neg, _normalize(q)), r

    def check_invariant(self) -> None:
        """Raise AssertionError if the representation is not normalized."""
        assert all(0 <= d < self.base for d in self.limbs)
        assert not self.limbs or self.limbs[-1] != 0
        assert self.limbs or not self.neg


# -------------------------------------------------------------------- magnitudes


def _normalize(a: list[int]) -> list[int]:
    """Drop the zero limbs at the top."""
    while a and a[-1] == 0:
        a.pop()
    return a


def _cmp_mag(a: list[int], b: list[int]) -> int:
    """Compare two normalized magnitudes: -1, 0 or 1."""
    if len(a) != len(b):
        return -1 if len(a) < len(b) else 1
    for i in range(len(a) - 1, -1, -1):
        if a[i] != b[i]:
            return -1 if a[i] < b[i] else 1
    return 0


def _add_mag(a: list[int], b: list[int], base: int) -> list[int]:
    """|a| + |b|, with one carry pass."""
    if len(a) < len(b):
        a, b = b, a
    s = []
    carry = 0  # @line L7
    for i in range(len(a)):  # @line L8
        t = a[i] + (b[i] if i < len(b) else 0) + carry  # @line L9
        if t >= base:  # @line L10
            s.append(t - base)
            carry = 1
        else:
            s.append(t)
            carry = 0
    if carry:  # @line L11
        s.append(1)
    return s  # @line L12


def _sub_mag(a: list[int], b: list[int], base: int) -> list[int]:
    """|a| - |b| for |a| >= |b|, with one borrow pass, normalized."""
    s = []
    borrow = 0  # @line L13
    for i in range(len(a)):  # @line L14
        t = a[i] - (b[i] if i < len(b) else 0) - borrow  # @line L15
        if t < 0:  # @line L16
            s.append(t + base)
            borrow = 1
        else:
            s.append(t)
            borrow = 0
    s = _normalize(s)  # @line L17
    return s  # @line L18
test_multiprecision_integer.py 126 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/multiprecision-integer/. The authors are listed in prov/WZN2G1.jsonl.

import random

import pytest

from multiprecision_integer import MultiprecisionInteger as MP


def mp(x: int, w: int = 4) -> MP:
    return MP.from_int(x, w)


def test_example() -> None:
    x = MP.from_limbs(False, [13, 15, 15], 4)
    assert x.to_int() == 4093
    y = mp(3)
    s = x.add(y)
    assert s.limbs == [0, 0, 0, 1] and s.to_int() == 4096
    d = x.add(mp(-4096))
    assert d.neg and d.limbs == [3] and d.to_int() == -3
    d.check_invariant()


def test_zero_and_one() -> None:
    z = MP(4)
    assert z.limbs == [] and not z.neg and z.to_int() == 0
    one = mp(1)
    assert one.sub(one).limbs == [] and not one.sub(one).neg
    assert mp(-5).add(mp(5)).to_int() == 0 and not mp(-5).add(mp(5)).neg
    assert mp(-5).mul(MP(4)).to_int() == 0 and not mp(-5).mul(MP(4)).neg
    assert z.neg_().to_int() == 0 and not z.neg_().neg
    assert MP.from_limbs(True, [0, 0], 4).to_int() == 0
    assert not MP.from_limbs(True, [0, 0], 4).neg
    assert one.mul(mp(-7)).to_int() == -7


def test_all_limbs_max() -> None:
    for w in (1, 4, 8):
        base = 1 << w
        for n in range(1, 7):
            top = base**n - 1
            x = MP.from_limbs(False, [base - 1] * n, w)
            assert x.to_int() == top
            assert x.add(mp(1, w)).limbs == [0] * n + [1]
            assert x.mul(x).to_int() == top * top
            assert x.add(x).to_int() == 2 * top
            assert mp(base**n, w).sub(mp(1, w)).limbs == [base - 1] * n


def test_compare() -> None:
    vals = [-300, -256, -255, -16, -1, 0, 1, 15, 16, 255, 4096]
    for a in vals:
        for b in vals:
            want = (a > b) - (a < b)
            assert mp(a).compare(mp(b)) == want


def test_divmod_limb() -> None:
    q, r = mp(4093).divmod_limb(10)
    assert (q.to_int(), r) == (409, 3)
    q, r = mp(-4093).divmod_limb(10)
    assert (q.to_int(), r) == (-409, 3)
    q, r = mp(5).divmod_limb(7)
    assert q.limbs == [] and r == 5
    with pytest.raises(ValueError):
        mp(5).divmod_limb(16)
    with pytest.raises(ValueError):
        mp(5).divmod_limb(0)


def test_bad_input() -> None:
    with pytest.raises(ValueError):
        MP.from_limbs(False, [16], 4)
    with pytest.raises(ValueError):
        mp(1, 4).add(mp(1, 8))
    with pytest.raises(ValueError):
        MP(0)


def rand_value(rng: random.Random, w: int) -> int:
    kind = rng.randrange(5)
    n = rng.randint(0, 12)
    if kind == 0:
        v = (1 << (w * n)) - 1
    elif kind == 1:
        v = 1 << (w * n)
    else:
        v = rng.getrandbits(w * n) if n else 0
    return -v if rng.random() < 0.5 else v


def test_random_against_int() -> None:
    rng = random.Random(2026)
    for _ in range(2000):
        w = rng.choice((1, 2, 4, 8))
        a, b = rand_value(rng, w), rand_value(rng, w)
        x, y = mp(a, w), mp(b, w)
        for r, want in ((x.add(y), a + b), (x.sub(y), a - b), (x.mul(y), a * b), (x.neg_(), -a)):
            r.check_invariant()
            assert r.to_int() == want
        assert x.compare(y) == (a > b) - (a < b)
        d = rng.randrange(1, 1 << w)
        q, r = x.divmod_limb(d)
        q.check_invariant()
        assert abs(a) == abs(q.to_int()) * d + r and 0 <= r < d
        assert q.to_int() == 0 or (q.to_int() < 0) == (a < 0)


def test_random_sequences() -> None:
    rng = random.Random(77)
    for _ in range(200):
        w = rng.choice((4, 8))
        x, v = MP(w), 0
        for _ in range(30):
            b = rand_value(rng, w) // (rng.randint(1, 3))
            op = rng.randrange(3)
            if op == 0:
                x, v = x.add(mp(b, w)), v + b
            elif op == 1:
                x, v = x.sub(mp(b, w)), v - b
            elif abs(v) < 1 << 200:
                x, v = x.mul(mp(b % 1000 - 500, w)), v * (b % 1000 - 500)
            x.check_invariant()
            assert x.to_int() == v

What the next level needs

  • the tests did not run in this build

Read the full page about Multiprecision integer representation