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
# 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