Split Money Amounts with Decimal Without Loss

Split a Decimal amount across integer weights while keeping cent-level totals exact, distributing leftover cents deterministically.

PythonData Integrity

Code

snippet
from __future__ import annotations

from decimal import ROUND_HALF_UP, Decimal


def split_amount(total: str, weights: list[int]) -> list[Decimal]:
    amount = Decimal(total).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    if amount < 0:
        raise ValueError("amount must be non-negative")
    if not weights or any(weight < 0 for weight in weights):
        raise ValueError("weights must be non-negative and non-empty")
    weight_sum = sum(weights)
    if weight_sum == 0:
        raise ValueError("weights must not sum to zero")

    cents = int(amount * 100)
    raw = [cents * weight for weight in weights]
    shares = [value // weight_sum for value in raw]
    remainder = cents - sum(shares)

    order = sorted(
        range(len(weights)),
        key=lambda index: (-(raw[index] % weight_sum), index),
    )
    for index in order[:remainder]:
        shares[index] += 1

    return [Decimal(share) / 100 for share in shares]


def main() -> None:
    thirds = split_amount("100.00", [1, 1, 1])
    print([str(part) for part in thirds], sum(thirds))
    assert sum(thirds) == Decimal("100.00")

    uneven = split_amount("0.05", [1, 1, 2])
    print([str(part) for part in uneven])
    assert [str(part) for part in uneven] == ["0.01", "0.01", "0.03"]

    try:
        split_amount("10.00", [0, 0])
    except ValueError as exc:
        print(f"rejected: {exc}")


if __name__ == "__main__":
    main()