A dash is not a zero
A learner scored 120 out of 300 on a project. The admin console showed 300%. The instructor console showed 0%. The student's own course card showed 12000%.
Nobody had written a bug. Every one of those numbers came out of code that was individually reasonable.
How one mark becomes three
Three applications each needed to turn a score into something a person reads. None of them had a shared helper for it, so each re-derived the arithmetic from whatever fields it happened to have in hand.
The API was not sending the denominator. So each app invented a fallback for it, and they invented four different ones:
| Where | Missing denominator became |
|---|---|
| Instructor console | 0 |
| Student course card | ?? 1 |
| Student progress row | || 25 |
| AI review rows, in seven places | a hard-coded 300 |
Divide 120 by each of those and you get the three numbers above, plus a division
by zero that rendered as Infinity% until something else swallowed it.
Every fallback was written by somebody being pragmatic about a value that was sometimes absent. Each is defensible on its own. Collectively they meant the platform had four opinions about a learner's grade and no way to notice.
The part that actually mattered
The wrong numbers were not the worst of it. None of them was marked as
uncertain. A confident 300% and a confident 72% render identically: same
font, same colour, same position on the card. A learner reading one has no signal
that the platform is guessing.
That is the failure worth naming. Not "we computed the wrong number" but "we presented a guess as a fact".
One rule
There is now exactly one module that turns a mark into something a human reads, and it is built on a single rule:
A missing denominator is unknown. It is not zero and it is not one.
Every function in it can return null, and callers render —.
export function scorePercent(score, total) {
const s = toNumber(score)
const t = toNumber(total)
if (s === null || t === null || t <= 0) return null
return Math.round((s / t) * 100)
}
Note what is not null: a score of zero. Zero is a real result and returns 0.
Only a missing or non-positive total makes the answer unknown. Conflating
"they scored nothing" with "we do not know what they scored" would reintroduce
the original bug from the other direction.
Being silent about a mark we cannot compute is recoverable — somebody asks, and we find out why the denominator was missing. Being confidently wrong about a learner's grade is not.
The dash is a diagnostic
An unexpected benefit: because the dash means one specific thing, it points at one specific cause.
A dash where a number belongs almost always means an assignment was created with no maximum score. That is a two-minute fix by whoever authored it. Before, the same condition produced a plausible-looking percentage that nobody ever questioned, so the underlying data stayed broken indefinitely.
A visible unknown gets fixed. A silent guess does not.
Totals above 100
The same module owns one more decision that used to be made three different ways.
Our grading engine adds bonus points on top of the 100 that the weighted categories divide between them, and only clamps the sum when the course's grade scale says extra credit may not exceed full marks. Where a scale allows it, a final mark of 108 is a real and correct result.
Rendering that as 108 / 100 (108%) states a ceiling that is not the ceiling and
a percentage that reads as arithmetic gone wrong. So there is one predicate for
"is this over the scale", and callers drop the denominator when it is true.
Three surfaces render that total — both consoles and the learner's own progress page — and before there was one predicate to agree on, they disagreed about it.
What we would say to anyone with the same shape of problem
A formatting helper is not a formatting concern when what it formats is somebody's grade. It decides a number that affects a person, and it belongs in one tested place for the same reason the grading engine does.
Count your fallbacks. Four different defaults for one missing value is not four small pragmatic decisions. It is the absence of a decision, made four times.
Make uncertainty visible in the output. If your renderer cannot distinguish "zero" from "unknown", your users cannot either, and neither can you when you go looking.