Batu Lab NotesPractical developer guides

Normalize percentages so the displayed total is exactly 100

By Batu · English technical notes

To normalize percentages so the displayed total is exactly 100, calculate the unrounded shares, truncate each to the display unit, then distribute the remaining hundredths to the largest fractional remainders. For three equal Decimal('1') weights, independently rounded shares display as 33.33 each, totaling 99.99. The largest-remainder result is 33.34, 33.33, and 33.33, totaling 100.00.

The code first preserves the original weights and computes exact Decimal shares. quantize(..., ROUND_DOWN) establishes a known base allocation at two decimal places. The one missing hundredth is then assigned to the largest remainder. Equal remainders need a tie rule; this example uses the earlier input index, making the first item receive the extra hundredth. The assertions establish the chosen allocation policy for this fixture, rather than proving that it is the only fair policy.

Use Decimal values created from strings or integers when decimal display rules matter. Constructing a Decimal from a binary float can preserve a float approximation instead. This example assumes non-negative weights with a positive total; production code should reject an empty or zero-total input and specify rules for negatives.

Decimal.quantize and explicit rounding modes are described in Python’s decimal documentation. ROUND_DOWN and the APIs used here are available in supported Python 3 releases; no newer API is required.

AI assistance disclosure: this article was prepared with AI assistance and checked against the shown synthetic fixture.

Source: Python decimal module.

from decimal import Decimal, ROUND_DOWN

weights = [Decimal("1"), Decimal("1"), Decimal("1")]
unit = Decimal("0.01")
total = sum(weights)
raw = [weight / total * Decimal("100") for weight in weights]

independent = [share.quantize(unit) for share in raw]
base = [share.quantize(unit, rounding=ROUND_DOWN) for share in raw]
remaining = int((Decimal("100.00") - sum(base)) / unit)
remainder_order = sorted(
    range(len(raw)),
    key=lambda index: (raw[index] - base[index], -index),
    reverse=True,
)
allocated = base[:]
for index in remainder_order[:remaining]:
    allocated[index] += unit

assert weights == [Decimal("1"), Decimal("1"), Decimal("1")]
assert independent == [Decimal("33.33")] * 3
assert allocated == [Decimal("33.34"), Decimal("33.33"), Decimal("33.33")]

print("weights:", weights)
print("independent:", ", ".join(f"{share:.2f}" for share in independent), "total:", f"{sum(independent):.2f}")
print("largest remainder:", ", ".join(f"{share:.2f}" for share in allocated), "total:", f"{sum(allocated):.2f}")
weights: [Decimal('1'), Decimal('1'), Decimal('1')]
independent: 33.33, 33.33, 33.33 total: 99.99
largest remainder: 33.34, 33.33, 33.33 total: 100.00