Grisu in Python
Fast shortest float printing using 64-bit integers, falling back to exact code when it cannot decide.
- File
grisu.py, 162 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/grisu/. The authors are listed in prov/PHVDFD.jsonl.
"""Grisu3 (Loitsch, 2010): shortest float printing with 64-bit integers and a certified result.
All arithmetic in grisu3 stays below 2^64, as it would in C with uint64_t. When grisu3 cannot
prove that its digits are the shortest and nearest, it returns None, and `shortest` falls back
to an exact method with rationals.
"""
import math
import struct
from fractions import Fraction
ALPHA, GAMMA = -60, -32 # target range for the binary exponent of the scaled numbers
MASK64 = (1 << 64) - 1
def _cached_power(k: int) -> tuple[int, int]:
"""10^k as (f, e) with 2^63 <= f < 2^64, f = round(10^k / 2^e). Cost: O(1) big-integer operations."""
v = Fraction(10) ** k
e = (v.numerator.bit_length() - v.denominator.bit_length()) - 64
while v / Fraction(2) ** e >= 1 << 64:
e += 1
while v / Fraction(2) ** e < 1 << 63:
e -= 1
f = math.floor(v / Fraction(2) ** e + Fraction(1, 2))
if f == 1 << 64:
f, e = 1 << 63, e + 1
return f, e
# Loitsch's table: 10^k for k = -348, -340, ..., 340, each rounded to a 64-bit significand.
CACHED = [(k, *_cached_power(k)) for k in range(-348, 341, 8)]
def _times(a: int, b: int) -> int:
"""The upper 64 bits of the 128-bit product, rounded (half up). Cost: O(1)."""
return (a * b + (1 << 63)) >> 64
def _normalize(f: int, e: int) -> tuple[int, int]:
shift = 64 - f.bit_length()
return f << shift, e - shift
def _round_weed(digits: list[int], dist_high_w: int, unsafe: int, rest: int, ten_kappa: int, unit: int) -> bool:
"""Move the last digit toward w and decide whether the result is certainly shortest and nearest."""
small, big = dist_high_w - unit, dist_high_w + unit # w lies strictly between too_high - big and too_high - small
while (rest < small and unsafe - rest >= ten_kappa # @line L8
and (rest + ten_kappa < small or small - rest >= rest + ten_kappa - small)):
digits[-1] -= 1
rest += ten_kappa
if (rest < big and unsafe - rest >= ten_kappa # @line L9
and (rest + ten_kappa < big or big - rest > rest + ten_kappa - big)):
return False # the nearest candidate depends on where w lies in its error interval
return 2 * unit <= rest <= unsafe - 4 * unit # the candidate is inside the safe interval
def grisu3(x: float) -> tuple[str, int] | None:
"""Shortest digits of x as (digits, exponent), value = int(digits) * 10^exponent, or None.
Precondition: x finite and x > 0. When the result is not None it is the shortest decimal in
the rounding interval of x and the nearest to x among those. Cost: O(1) operations on
64-bit integers (at most 17 digit steps and one table lookup).
"""
bits = struct.unpack(">Q", struct.pack(">d", x))[0]
biased, frac = bits >> 52, bits & ((1 << 52) - 1)
f, e = (frac, -1074) if biased == 0 else (frac | (1 << 52), biased - 1075) # @line L1
plus_f, plus_e = _normalize(2 * f + 1, e - 1) # @line L2, x + g+, exactly
if frac == 0 and biased > 1:
minus_f, minus_e = 4 * f - 1, e - 2 # x - g-, where the lower gap is half as wide
else:
minus_f, minus_e = 2 * f - 1, e - 1
minus_f <<= minus_e - plus_e
w_f, w_e = _normalize(f, e)
# @line L3: a cached power 10^K with ALPHA <= w_e + c_e + 64 <= GAMMA
K, c_f, c_e = next(t for t in CACHED if ALPHA <= w_e + t[2] + 64 <= GAMMA)
w = _times(w_f, c_f) # @line L4, each within one unit of the exact scaled value
high = _times(plus_f, c_f)
low = _times(minus_f, c_f)
e2 = w_e + c_e + 64 # the three products share this binary exponent; it is in [ALPHA, GAMMA]
too_low, too_high = low - 1, high + 1 # @line L5, an interval that surely contains R(x)
unsafe = too_high - too_low
one_shift = -e2
one = 1 << one_shift
integrals, fractionals = too_high >> one_shift, too_high & (one - 1)
kappa = len(str(integrals))
divisor = 10 ** (kappa - 1)
digits: list[int] = []
while kappa > 0: # @line L6, digits of the integer part of too_high
digits.append(integrals // divisor)
integrals %= divisor
kappa -= 1
rest = (integrals << one_shift) + fractionals
if rest < unsafe: # @line L7, the truncation lies above too_low
ok = _round_weed(digits, too_high - w, unsafe, rest, divisor << one_shift, 1)
return ("".join(map(str, digits)), kappa - K) if ok else None
divisor //= 10
unit = 1
while True: # digits of the fractional part
fractionals *= 10
unit *= 10
unsafe *= 10
digits.append(fractionals >> one_shift)
fractionals &= one - 1
kappa -= 1
if fractionals < unsafe:
ok = _round_weed(digits, (too_high - w) * unit, unsafe, fractionals, one, unit)
return ("".join(map(str, digits)), kappa - K) if ok else None # @line L10
def exact_shortest(x: float) -> tuple[str, int]:
"""The fallback: shortest and nearest digits by exact rational arithmetic. Cost: O(17) Fraction operations."""
bits = struct.unpack(">Q", struct.pack(">d", x))[0]
fx = Fraction(x)
up = Fraction(math.nextafter(x, math.inf)) if bits != 0x7FEFFFFFFFFFFFFF else Fraction(2) ** 1024
lo, hi = (fx + Fraction(math.nextafter(x, 0.0))) / 2, (fx + up) / 2
closed = bits % 2 == 0
def inside(y: Fraction) -> bool:
return lo <= y <= hi if closed else lo < y < hi
t = len(str(math.floor(hi))) if hi >= 1 else -len(str(math.floor(1 / hi)))
while True: # the coarsest power of ten with a multiple inside R
u = Fraction(10) ** t
c0 = math.floor(fx / u)
cands = [c for c in (c0, c0 + 1) if c > 0 and inside(c * u)]
if cands:
best = min(cands, key=lambda c: (abs(c * u - fx), c % 2))
s = str(best).rstrip("0")
return s, t + len(str(best)) - len(s)
t -= 1
def shortest(x: float) -> tuple[str, int]:
"""Grisu3 with the exact fallback, so always correct. Precondition: x finite, x > 0. Cost: O(1) usually."""
r = grisu3(x)
return r if r is not None else exact_shortest(x)
def to_string(x: float) -> str:
"""Format x in the style of Python's repr. Cost: as shortest."""
if math.isnan(x):
return "nan"
if math.isinf(x):
return "inf" if x > 0 else "-inf"
if x == 0.0:
return "-0.0" if math.copysign(1.0, x) < 0 else "0.0"
sign = "-" if x < 0 else ""
digits, exp10 = shortest(abs(x))
n = len(digits)
point = n + exp10 # value = 0.digits * 10^point
if -4 < point <= 16:
if point <= 0:
return sign + "0." + "0" * -point + digits
if point >= n:
return sign + digits + "0" * (point - n) + ".0"
return sign + digits[:point] + "." + digits[point:]
exp = point - 1
mant = digits[0] + ("." + digits[1:] if n > 1 else "")
return f"{sign}{mant}e{'+' if exp >= 0 else '-'}{abs(exp):02d}"
test_grisu.py 110 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/grisu/. The authors are listed in prov/PHVDFD.jsonl.
"""Tests for Grisu3: certified results are shortest and nearest; failures are rare and caught."""
import math
import random
import struct
from fractions import Fraction
from grisu import CACHED, exact_shortest, grisu3, shortest, to_string
MAX = 1.7976931348623157e308
def from_bits(b: int) -> float:
return struct.unpack(">d", struct.pack(">Q", b))[0]
def interval(x: float) -> tuple[Fraction, Fraction, bool]:
up = Fraction(math.nextafter(x, math.inf)) if x < MAX else Fraction(2) ** 1024
fx = Fraction(x)
bits = struct.unpack(">Q", struct.pack(">d", x))[0]
return (fx + Fraction(math.nextafter(x, 0.0))) / 2, (fx + up) / 2, bits % 2 == 0
def check(x: float, digits: str, exp10: int) -> None:
"""Exact check: inside R, no shorter number inside, nearest among equal-length multiples."""
lo, hi, closed = interval(x)
def inside(y: Fraction) -> bool:
return lo <= y <= hi if closed else lo < y < hi
u = Fraction(10) ** exp10
y = int(digits) * u
assert digits[0] != "0" and digits[-1] != "0" and inside(y)
fx = Fraction(x)
c = (fx // (10 * u)) * 10 * u
assert not (c > 0 and inside(c)) and not inside(c + 10 * u)
for z in (y - u, y + u):
assert not inside(z) or abs(z - fx) > abs(y - fx) or (abs(z - fx) == abs(y - fx) and int(digits) % 2 == 0)
def test_cached_powers_are_rounded_and_cover_the_range() -> None:
for k, f, e in CACHED:
assert 1 << 63 <= f < 1 << 64
assert abs(Fraction(f) - Fraction(10) ** k / Fraction(2) ** e) <= Fraction(1, 2)
for a, b in zip(CACHED, CACHED[1:]):
assert b[0] - a[0] == 8 and 0 < b[2] - a[2] < 28 # 28 = GAMMA - ALPHA
def test_examples() -> None:
assert grisu3(0.1) == ("1", -1)
assert grisu3(0.3) == ("3", -1)
assert grisu3(5e-324) == ("5", -324)
assert grisu3(MAX) == ("17976931348623157", 292)
assert grisu3(2.0 ** 63) == ("9223372036854776", 3)
def test_known_failures_fall_back() -> None:
# 1e23 is exactly the upper end of its rounding interval, and 2^50 + 1/4 is a tie between
# two shortest strings; Grisu3 cannot decide either with 64-bit approximations.
assert grisu3(1e23) is None and shortest(1e23) == ("1", 23)
assert grisu3(2.0 ** 50 + 0.25) is None
assert to_string(2.0 ** 50 + 0.25) == "1125899906842624.2"
def test_specials() -> None:
assert to_string(0.0) == "0.0" and to_string(-0.0) == "-0.0"
assert to_string(math.inf) == "inf" and to_string(math.nan) == "nan"
assert to_string(-1.5) == "-1.5"
for e in range(-1074, 1024, 7):
assert to_string(2.0 ** e) == repr(2.0 ** e)
def test_random_patterns_certified_results_are_exact() -> None:
rng = random.Random(2010)
failures = total = 0
for _ in range(30000):
x = from_bits(rng.getrandbits(63))
if x == 0.0 or not math.isfinite(x):
continue
total += 1
r = grisu3(x)
if r is None:
failures += 1
assert exact_shortest(x) == shortest(x)
continue
assert to_string(x) == repr(x)
if total % 20 == 0:
check(x, *r)
assert failures / total < 0.01 # about 0.5% in Loitsch's measurements and here
def test_fallback_matches_repr() -> None:
rng = random.Random(4)
for _ in range(3000):
x = from_bits(rng.getrandbits(63))
if x == 0.0 or not math.isfinite(x):
continue
d, e = exact_shortest(x)
assert float(f"{d}e{e}") == x
check(x, d, e)
assert to_string(x) == repr(x)
def test_small_subnormals() -> None:
for m in range(1, 2000):
x = m * 5e-324
assert to_string(x) == repr(x)
What the next level needs
- the tests did not run in this build