Topic 71 of 91· Software engineering and digital delivery

Currency-Safe Cost Rollup

Summing many money line items — monthly invoices, per-site costs, multi-year budget-impact figures — with ordinary binary floating-point (f64) numbers accumulates small representation errors, because most decimal fractions ($1,234.56, for instance) aren't exactly representable in binary floating point. Each individual error is tiny, but a large model summing hundreds or thousands of line items over several years can drift by fractions of a cent — and the drift depends on the order the additions happen in, which makes it non-reproducible. A currency rollup done in exact decimal (or integer minor-unit) arithmetic sums exactly, matching how accounting systems and double-entry ledgers must reconcile to the cent.

Why it matters

This is a well-documented, foundational class of software bug: Goldberg's 1991 ACM Computing Surveys paper "What Every Computer Scientist Should Know About Floating-Point Arithmetic" is the standard reference for exactly why binary floating point cannot represent most decimal money values exactly, and why summing many of them compounds the error. Health-economics and NHS finance models routinely sum many years and many cost categories — total cost of ownership and budget-impact analysis both aggregate large numbers of f64 cost line items over multi-year horizons. When a model must reconcile to the cent — an audit recomputing the total by hand must get the identical figure — the arithmetic itself has to be exact-decimal, not floating point.

The math

Naive rollup:              total = Σ f64(line_item_i)         — order-dependent drift
Currency-safe rollup:      total = Σ Decimal(line_item_i)      — exact, reproducible

Applying a percentage adjustment (e.g. a contingency buffer):
  adjusted = total × multiplier                — exact Decimal result, may carry
                                                  more decimal places than the
                                                  currency's minor-unit exponent
  rounded  = round(adjusted, currency_exponent, rounding_rule)  — the rounding rule
                                                  (half-up vs half-even/banker's
                                                  rounding) must be stated explicitly

Note the two-step discipline: multiplying an exact Decimal amount by a multiplier can produce more decimal places than the currency actually uses (three decimal places from a two-decimal-place amount times a two-decimal-place multiplier, for instance) — that intermediate precision is not automatically rounded away; only an explicit rounding step, with a stated rounding rule, takes it down to the currency's real minor-unit exponent.

Worked example

Twelve identical monthly invoices of $1,234.56 each, summed in exact decimal arithmetic: $1,234.56 × 12 = $14,814.72, exactly. Contrast this with summing the f64 literal 1234.56 twelve times in IEEE-754 double precision, which can drift by fractions of a cent depending on the order of summation — a real, documented class of bug, not a problem for a model built on exact decimal Money arithmetic.

Now apply a standard budget-impact contingency buffer of 5% (a 1.05× multiplier) to that $14,814.72 total: $14,814.72 × 1.05 = $15,555.456 — three decimal places, because the multiplication is exact and not automatically rounded to the currency's two decimal places. Rounding that explicitly to 2 decimal places using banker's rounding (half-even) gives exactly $15,555.46.

Software engineering connection

This is the direct, foundational lesson behind "financial software uses Decimal, not float" — it connects explicitly to this repo's total cost of ownership and budget-impact analysis modules, both of which currently sum plain floating-point costs; the correctness argument here doesn't demand migrating those models immediately, but it does state precisely when a system must reconcile to the cent and therefore must not use binary floating point for its money arithmetic. See also exact-cents cost allocation for the companion problem of splitting (rather than summing) totals without losing cents.

Pitfalls

  • Converting to float mid-chain: pulling a money value out to a floating-point number partway through a calculation (some Money libraries even name this conversion method something like "lossy" as an explicit warning) silently discards the exactness guarantee for every calculation downstream of that point.
  • "Decimal is too slow to bother with": dismissing exact-decimal arithmetic as unnecessary overhead when correctness and auditability — not raw throughput — are what matter for financial reporting.
  • Applying a contingency percentage without stating the rounding rule: rounding half-up versus half-even (banker's rounding) can change the final cent; the rounding convention itself must be a stated, auditable choice — see cost-benefit analysis for the HM Treasury Green Book guidance on contingency and optimism-bias adjustments, which is exactly the kind of figure this rounding step is applied to.

Sources