A bag of QUIKRETE concrete mix that weighs 80 lb yields 0.60 cubic feet. If your job needs 4.2 cubic feet, you need exactly 7 bags.
Math.ceil(4.2 / 0.6) // 8
4.2 / 0.6 // 7.000000000000001
JavaScript says 8. That's one wasted bag and an annoyed customer, from a bug that no amount of looking at the code will show you. Between 1 and 200 bags' worth of volume, the naive Math.ceil(volume / perBag) gets 10 of the 200 exact multiples wrong.
I build Toggle9, a set of free calculators that show their working, and bugs like this are the reason every tool goes through the same testing routine. Here it is.
1. Start from the source, not the code
Every number a tool uses comes from a primary source: a standard (NIST defines 1 foot as exactly 0.3048 m), a government table, or a manufacturer's data sheet. For concrete, the bag yields come straight from the QUIKRETE technical data sheet, and the code says so:
// QUIKRETE Concrete Mix (No. 1101) technical data sheet: yield per bag in ft³.
export const BAGS = [
{ lb: 40, ft3: 0.30 },
{ lb: 50, ft3: 0.375 },
{ lb: 60, ft3: 0.45 },
{ lb: 80, ft3: 0.60 },
{ lb: 90, ft3: 0.675 },
];
2. Work the examples by hand, before writing code
Before any code exists, I work a few examples by hand from the source, including the awkward ones: tiny jobs, huge jobs, mixed units, and exact multiples, which is where the 4.2 ÷ 0.6 bug lives.
A worked example for a patio slab, 12 ft × 10 ft × 4 in:
- volume = 12 × 10 × (4/12) = 40 ft³
- plus 10% for waste = 44 ft³ = 44 / 27 = 1.63 yd³
- 80 lb bags: 44 / 0.60 = 73.3 → 74 bags
- 60 lb bags: 44 / 0.45 = 97.8 → 98 bags
- ready-mix is sold in quarter yards: 1.63 → 1.75 yd³
Those numbers are worked out by hand, not taken from the code's output. That matters: a test that copies whatever the code printed only proves the code is consistent with itself.
3. Keep the maths in its own file
Each tool's calculation lives in a compute.js that only does maths: no DOM, no formatting, no event handlers. The UI imports it. That rule means a layout change can never change an answer, and the maths can be tested in Node with nothing mocked.
// tools/concrete/compute.js (excerpt)
const n = Math.ceil(ft3 / b.ft3 - 1e-9);
The - 1e-9 is the fix for the opening bug: subtract a tolerance far smaller than any real measurement before rounding up, so floating-point noise above an exact multiple doesn't cost an extra bag.
4. Hand-worked examples become tests
Tests use Node's built-in node:test and assert, with no framework. Values are kept in SI units inside the engine, so tests convert at the edges:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { partVolume, totals, FT3 } from '../tools/concrete/compute.js';
const FT = 0.3048, IN = 0.0254;
const close = (a, b, eps = 1e-6) => assert.ok(Math.abs(a - b) < eps, `${a} != ${b}`);
test('slab 12 ft x 10 ft x 4 in = 40 ft³', () => {
close(partVolume('slab', { l: 12 * FT, w: 10 * FT, t: 4 * IN }) / FT3, 40);
});
test('totals with 10% waste: 44 ft³, 1.63 yd³, 74 x 80 lb bags, 98 x 60 lb', () => {
const t = totals(40 * FT3, 10, 0.25, 'us');
close(t.ft3, 44); close(t.yd3, 44 / 27);
assert.equal(t.bags.find(b => b.lb === 80).n, 74);
assert.equal(t.bags.find(b => b.lb === 60).n, 98);
close(t.order, 1.75); // 1.63 yd³ rounds up to 1.75 in quarter-yard steps
});
test('exact bag multiples do not round up an extra bag', () => {
const t = totals(6 * FT3, 0, 0, 'us'); // 6 ft³ = exactly 10 x 80 lb
assert.equal(t.bags.find(b => b.lb === 80).n, 10);
});
The test names are the worked examples, so a failure reads like a sentence: "steps: 3 steps, 7 in rise, 11 in run, 4 ft wide = 12.83 ft³".
Today the engine has 202 of these across 30 test files, and npm test runs them in about half a second. A tool can't go live unless every one passes, and they all run again before every release.
5. Then check it in real browsers
Unit tests prove the maths. They don't prove that typing 12' 6" into the real input on a phone produces the right number on screen. For that there's a scripted pass (npm run check) that loads every tool in real Chrome and WebKit, at phone and desktop widths, and runs each tool through a scenario:
await typeInto(page, root + ' .t9-part .t9-fields .t9-input', `12' 6"`);
out.push(`length 12' 6": → ${await txt(page, root + ' .t9-fig')}`);
await typeInto(page, root + ' .t9-part .t9-fields .t9-input', `abc`);
out.push(`bad input error: "${await txt(page, root + ' .t9-part .t9-fields .t9-err')}"`);
It also reports console errors and horizontal overflow (a page that's wider than the phone), and a separate pass runs an automated accessibility check with axe-core. WebKit is there because iPhone Safari is where the surprises are.
6. Publish the receipts, and re-check
Each tool page shows "Show the math" with the formula, the sources it uses with links, and the date they were last checked. Sources are re-checked every 12 months, because data sheets and government tables change, and corrections are logged publicly.
The full process, with a real example worked from start to finish, is on How I check every tool. The concrete example above is the concrete calculator, so you can check my 74 bags yourself.
What's the nastiest floating-point rounding bug you've run into? This one would have cost someone a bag of concrete.
Top comments (0)