Three kinds of number in a unit-conversion table

A note on why 236.5882365, 236.164 and 125 are not the same kind of fact, and why a build pipeline should treat them differently.

Write a kitchen-conversion page and you end up with a table of numbers that all look alike: a cup is this many millilitres, this many grams of water, this many grams of flour. They print at the same width, in the same font, and a reader has no way to tell them apart. But they are three different species of claim, and only one of them can be verified by recomputation. Confusing them is how conversion tables end up quietly wrong.

1. Exact by definition

Some values are not measurements at all. They are the output of a chain of definitions, and the chain terminates. The US customary cup is one of these.

The chain, in order:

Every digit is reproducible from four definitions. Nothing was ever weighed or poured. The same is true of the avoirdupois ounce: the international pound is defined as exactly 0.45359237 kg, so an ounce is 453.59237 / 16 = 28.349523125 g — again exact, again derivable without touching a scale.

$ python3 -c "print(231*2.54**3/128*8)"
236.5882365
$ python3 -c "print(453.59237/16)"
28.349523125

The practical consequence: a value of this species should never be fetched from a web page and trusted. It should be recomputed from its definition at build time. A definitional constant that a pipeline copies from a source is a constant that can be silently corrupted by a typo upstream; a definitional constant the pipeline derives cannot be, because the derivation either produces the digits or it does not. This is also the only species where checking every digit is meaningful — 236.5882365 is right to the last 5, and rounding it to 236.59 is a choice about presentation, not a loss of accuracy.

2. Measured, with conditions attached

Now take grams of water per cup. This one cannot be derived. It is a volume times a density, and the density of liquid water is a function of temperature — it peaks near 4 °C and falls away on both sides. So the number is only defined once you fix a condition.

Work backwards from a published table value of 236.164 g per US cup:

236.164 / 236.5882365 = 0.99820686...   g/cm³

That implied density corresponds to water at approximately 20 °C. Which is fine — 20 °C is a reasonable room-temperature reference — but notice what happened: the condition was not printed next to the number, and had to be reverse-engineered from it. A reader at a different temperature, or a pipeline that later sources the same figure from a table built at 25 °C or 4 °C, gets a different value and no signal that anything changed.

Reference tempDensity (g/cm³, approx.)Grams per US cup
4 °C0.99997236.581
20 °C0.99821236.164
25 °C0.99705235.890

The spread across ordinary kitchen temperatures is under a gram — irrelevant for cooking, and that is exactly why it slips through. The problem is not the magnitude of the error. The problem is that a value with a hidden parameter looks identical to a value without one, so a verification step that only asks "does this number match the source?" will pass it forever without ever asking "match the source under which conditions?"

The handling rule that follows: a measured value must carry its conditions as data, not as prose. If the condition cannot be recorded, the value is not verifiable, and the honest move is to publish the range rather than a false-precision point.

3. Conventional — true only because people agreed

The third species is the one that breaks naive verification hardest. "A cup of flour is 125 grams" is not exact and it is not measured in the sense above. It is a convention. Flour has no fixed density: it depends on the wheat, the mill, the humidity, whether the cup was scooped or spooned-and-levelled, and whether it was tapped down. Different reputable sources publish different values for the same ingredient, and none of them is wrong, because there is no underlying quantity for them to disagree about.

A number like this fails the test that the other two pass. You cannot recompute it; there is no definition. You cannot re-measure it into agreement; the measurement is method-dependent by nature. All you can do is attribute it: this source, this method, this date. The moment a pipeline strips the attribution and prints the bare figure, it has converted an opinion into a fact.

The same reasoning explains why some entries in a conversion table have no defensible value at all. Butter is the classic case: it is sold in sticks in some markets and as a block in others, it is soft or hard depending on temperature, and "a cup of butter" describes a packing operation more than a quantity. There is a genuinely correct output here, and it is to ship the table without that row.

The rule the three species imply

Once you separate them, a build-time policy writes itself, and it is not a style guide — it is three different verification procedures:

SpeciesVerified byFails whenCorrect failure behaviour
Exact by definitionRecomputation from the defining chainDerived digits differ from stored digitsHalt. One of them is a typo.
Measured with conditionsRe-fetch plus condition matchSource omits or changes the conditionPublish a range, or hold the value.
ConventionalAttribution onlyAttribution is missing or stalePrint the source inline, or drop the row.

Notice that only the first row can ever produce a clean pass/fail. That is worth saying plainly, because a pipeline that reports "all values verified" across a mixed table is reporting something that cannot be true — it has applied a definitional test to conventional data and got a green tick out of it, which is worse than no test at all.

The useful invariant: a generator should never be the thing that types a number. If code derives it, the derivation is the proof. If code fetches it, the fetch and its conditions are the proof. If neither is possible, there is no proof, and the row should not ship.

Why the held row is the signal

The counter-intuitive part of all this is that a table with a gap in it is stronger evidence of care than a table without one. A complete table tells you nothing about the process that produced it — the completeness is equally consistent with rigorous sourcing and with a model filling in plausible-looking numbers, and from the outside those two look the same. A table with a row visibly withheld tells you the process has a state other than "publish". That is the only observable that distinguishes them.

This is the design stance behind toolsthatrank.com, which builds tool sites under a rule that the model never states a figure and code fetches, recomputes and re-confirms each value before a page ships — and holds the page when a value will not prove.