Algorithm Search⌘K

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

Read the full page about Grisu