Formatting and locale / Numbers and money
Where the half cent goes, and why the document disagrees with itself
Which way a value exactly halfway between two representable amounts is rounded, and at which point in the calculation the rounding happens.
The defect
Two separate decisions get conflated. The first is the rule: rounding half away from zero and rounding half to even are both standard, they disagree on exactly the halfway cases, and over many rows the difference accumulates in a direction. The second, and the one that actually produces wrong documents, is the stage: rounding each line and summing the rounded values gives a different total from summing the exact values and rounding once. Both are defensible, neither is wrong in general, and a document that does one in the line column and the other in the totals block disagrees with itself by a unit or two, which is the kind of discrepancy a reader will find and query.
What it does to the document
The visible symptom is a column that does not add up: the printed lines summed by hand give a different figure from the printed total, by one or two units. On a document with a tax breakdown beside the totals block, the two are twelve millimetres apart and every reader compares them.
The decisions
Reasons rather than a description of the code. Formatting rules are data that changes, so most of these are about taking the rule from a maintained source rather than encoding today's answer.
- Decide the rule and the stage once, write both down, and use the same pair everywhere in the system. The consistency matters more than which pair you picked.
- Round for display from the same value you rounded for arithmetic. Displaying a value rounded differently from the one that was summed is how a column stops adding up.
- Do the arithmetic in a decimal type rather than a binary floating point one, so the values being rounded are the values you think they are.
- Where a jurisdiction or a contract specifies a rule, that specification wins and the rest of the system conforms to it rather than the other way round.
- Put the residue somewhere deliberate when splitting an amount. If three shares do not divide evenly, decide which share absorbs the remainder rather than letting the loop decide.
- Never re-round an already-rounded figure. Each pass can move it again, and the second pass is always a bug.
In code
The formatting happens before the markup exists, so this is mostly code rather than CSS. The wrong version is kept in a comment where the wrong version is the thing people write.
// Decide the rule and the stage, then use the same pair everywhere.
//
// rule: half away from zero | half to even
// stage: round per line | round once at the end
//
// Whichever pair, display the value that was actually summed.
// Work in a decimal representation, not binary floating point, so the
// value being rounded is the value you think it is.
const lineNet = [124055n, 63999n, 18004n]; // minor units
const total = lineNet.reduce((a, b) => a + b, 0n);
const format = (minor) =>
new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" })
.format(Number(minor) / 100);
// Splitting: decide which share takes the residue rather than letting
// the loop decide it.
function split(totalMinor, ways) {
const base = totalMinor / BigInt(ways);
const shares = Array.from({ length: ways }, () => base);
shares[ways - 1] += totalMinor - base * BigInt(ways); // last absorbs it
return shares;
}What people do instead
Computing the total from the already-rounded line figures because those are the numbers on the page. Every line's rounding error is carried into the sum, the errors do not cancel, and on a long document the total is visibly wrong. Sum the exact values and round the result, then display that.
How to find out rather than assume
Build a document with values that round in both directions, including at least one exact half, then add the printed column by hand and compare with the printed total. Do it again with a document long enough for the errors to accumulate, which is where a per-line rounding bug becomes visible.
Where the API sits in this
Nowhere, and that is worth saying plainly. This API renders HTML to PDF. By the time markup reaches it, every value in it is already a string that somebody's code produced, so there is no locale option on the render because there would be nothing for one to do. What the render controls is the page: the size, the margins, and whether the formatted value still fits the column it landed in. That is why each of these pages has a paragraph about the layout consequence, because that is the half a document adds to the general problem.
Frequently asked
Should the document use the sender's conventions or the reader's?
That is a decision rather than a fact, and the only wrong answer is not making it. The issuer's conventions are defensible for a document that is a record of what the issuer did. The recipient's are defensible for a document meant to be acted on. What fails is a document that mixes them, because a reader who sees two conventions on one page cannot tell which one any given figure is in.
Is it enough to set the document language?
No. A language attribute helps with hyphenation, with the assistive technology reading order and with anything that needs to know what language the text is in, and it changes none of the values. A date formatted as 03/04/2026 is exactly as ambiguous in a document declared as English as in one declared as anything else. The values are formatted upstream.
Why does this belong in a document reference rather than a general one?
Because a document cannot be re-rendered by its reader. On a screen a wrong format is annoying and a user can often change a setting. On paper the format is final, the layout was tuned to the string lengths of one language, and a value that grew by a few characters can push a table onto another page. The formatting problem and the pagination problem are the same problem here.
Related formatting topics
The defects that travel together, then the rest of the same group.
Decimal and thousands separators, and the factor of a thousand
The comma and the point swap roles between conventions, so the same string of characters is two amounts a thousand apart depending on who is reading it.
Negative amounts in accounting format, and the missing minus
A negative figure written with a leading minus, with parentheses, or with a trailing sign, where the conventions differ and one of them is easy to miss entirely.
Units on a document, and the quantity with no unit at all
A measured quantity printed without its unit, in the wrong system for the reader, or separated from its unit by a line break.
Which side the currency symbol goes on, and why it matters
The symbol goes before the amount in some conventions and after it in others, with different spacing, and the choice affects both meaning and column alignment.
Tax breakdown by rate, as a layout problem
The block this value lands in, and what it takes to make it fit.
Every formatting and locale topic
The full list, grouped by money, time, identity and generated language.
Writing systems in a PDF
The other half: whether the glyphs of a script come out shaped and joined correctly.
Values are formatted by your code before they reach the markup, so every fix on this page is upstream of the render. What the render decides is whether the result still fits.