A reader who types a FAQ's own example into the calculator should get the FAQ's answer. I had never checked whether that is true on my site.
LandscapeCraft (landscapecraft.ai) has three small material calculators: mulch, gravel and pavers. The calculation functions and the page copy live in one file, lib/calculators.ts, and four of the nine FAQ answers contain a number the functions can reproduce. This post is the check: which numbers match, which ones only match under a condition the reader cannot see, and the one place where a floating-point guard decides the answer. The code is from the repository as of 2026-11-02 (commit a0694ab), and the live-page checks are from the same day. The one code comment below is translated from Chinese.
Where the numbers live
FAQ entries are plain strings in an array, one array per calculator:
// lib/calculators.ts (mulch calculator) { q: "How much does a cubic yard of mulch cover?", a: "At a depth of 3 inches, one cubic yard covers roughly 108 square feet. At 2 inches it covers about 162 square feet.", }, The page renders the same array twice, once as visible text and once as FAQPage JSON-LD:
// app/calculators/[slug]/page.tsx { "@type": "FAQPage", mainEntity: c.faqs.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: f.a }, })), }, On the three live pages, the 9 visible answers and the 9 JSON-LD answers are character-for-character identical. I am not claiming the markup helps search: Google's documentation updates page says the FAQ rich result "will no longer appear in Google Search starting May 7, 2026", and a later entry says its documentation was removed because the feature "is no longer shown in Google Search results". The reason it matters here is simpler: one wrong sentence in the array is published twice.
The check
The calculators are pure functions, so the check is a loop. Node 22.12 can import the TypeScript file directly with --experimental-strip-types. This is the first FAQ number, and I wrote the same loop for the others:
import * as m from "./lib/calculators.ts"; for (const extra of [0, 10]) { const v = m.computeVolume(108, 3, extra); // 108 sq ft, 3 in deep console.log(`${extra}%`, v.cuYdWithExtra.toFixed(2)); } // 0% 1.00 // 10% 1.10 The second case is the page default: in components/calculators/calculator.tsx the allowance starts as useState("10"), and all three calculators use that state. Here is every FAQ figure the functions can reproduce, with the function's answer at 0% and at the default 10%. Each area is the whole number the FAQ implies, and results are formatted to two decimals:
| FAQ says | Exact value | At 0% allowance | At the default 10% |
|---|---|---|---|
| Mulch, 3 in deep: 1 cu yd covers about 108 sq ft | 108 | 108 sq ft → 1.00 cu yd | 1.10 cu yd |
| Mulch, 2 in deep: about 162 sq ft | 162 | 162 sq ft → 1.00 cu yd | 1.10 cu yd |
| 1 cu yd is 13.5 bags of 2 cu ft, or 9 bags of 3 cu ft | 13.5 and 9 | 108 sq ft at 3 in → 14 and 9 bags | 15 and 10 bags |
| Gravel at 2 in, 1.4 t/cu yd: 1 ton covers about 115 sq ft | 115.71 | 115 sq ft → 0.99 t | 1.09 t |
| Gravel at 3 in: about 77 sq ft | 77.14 | 77 sq ft → 1.00 t | 1.10 t |
| 100 sq ft of 12 × 12 pavers with 10% extra: 110 | 110 | not applicable, the answer states 10% | 110 |
For the 108 sq ft case I also checked the live page: 12 ft by 9 ft with the defaults shows "Cubic yards 1.10" with the note "1.00 before the 10% allowance", "Cubic feet 29.7" and "Bags (2 cu ft) 15". The other two calculators I only ran as functions.
None of the FAQ figures is wrong as arithmetic. "About 115" is 115.71 rounded down, which "about" covers. The FAQ answers a geometry question (how much does a yard cover) and the calculator answers a buying question (how much should I order). Five of the six rows assume no allowance, while the default adds 10%. The page admits it in a small note next to the result, and the FAQ never mentions it, so a reader who tests the FAQ against the tool lands 10% off. The bag row shows a second effect: 13.5 is a conversion, but you cannot buy half a bag, so the calculator rounds up to 14 at 0% and to 15 at the default.
The one answer that includes the allowance
The pavers answer is the one FAQ number that does include the allowance: 100 sq ft of 12 × 12 pavers is 100 pavers, plus 10%. In JavaScript:
100 * 1.1; // 110.00000000000001 Math.ceil(100 * 1.1); // 111 The pavers formula is area × 144 ÷ (length × width), times (1 + waste ÷ 100), rounded up. Without a guard, the FAQ's own example would show 111 on the page next to an FAQ that says 110. The file has the guard:
/** Strip floating-point error before rounding up (e.g. 100 × 1.1 = 110.00000000000001 should not become 111) */ const ceilClean = (n: number) => Math.ceil(Math.round(n * 1e6) / 1e6); How often does it matter? Rather than argue, I compared three implementations over a grid: the five preset paver sizes, waste of 5, 10, 15 and 20 percent, and every whole area from 1 to 2,000 sq ft, which is 40,000 inputs. The reference is exact integer arithmetic with BigInt, which avoids floats entirely:
const ceilDiv = (a, b) => (a + b - 1n) / b; // ceil(area × 144 × (100 + waste) / (length × width × 100)), all whole numbers const exact = Number(ceilDiv(BigInt(area) * 144n * BigInt(100 + waste), BigInt(l * w) * 100n)); const naive = Math.ceil(((area * 144) / (l * w)) * (1 + waste / 100)); const clean = m.computePavers({ areaSqFt: area, paverLengthIn: l, paverWidthIn: w, wastePercent: waste, baseDepthIn: 0, sandDepthIn: 0 }).pavers; | Paver size (in) | Inputs | Unguarded Math.ceil differs from exact | ceilClean differs from exact |
|---|---|---|---|
| 12 × 12 | 8,000 | 112 | 0 |
| 12 × 24 | 8,000 | 54 | 0 |
| 16 × 16 | 8,000 | 7 | 0 |
| 4 × 8 | 8,000 | 52 | 0 |
| 6 × 9 | 8,000 | 187 | 0 |
| Total | 40,000 | 412 | 0 |
In all 412 differing cases the unguarded version asked for more pavers than exact arithmetic, and in none did it ask for fewer. For 12 × 12 pavers all 112 are at 10% waste, and the FAQ's 100 sq ft is one of them (50, 90, 100, 110 and 170 are the first five).
The guard has a price. It rounds to six decimals first, so an area a hair above a whole number counts as that whole number: in my run 100.0000004 sq ft gave 110 and 100.000001 gave 111. Nobody types those, so I think the trade is right, but it is a trade.
What I would change
I haven't changed any of this.
- Say what each number assumes. The three answers about mulch coverage, mulch bags and gravel coverage could say "before any allowance", or give both figures.
- Do not type derived numbers into copy. Build the answer strings from the same functions, so a changed default or formula changes the sentence too.
- Keep the grid comparison as a test. The repository has no test files (
package.jsonhasdev,build,startandlint), so today the guard is protected by a comment.
What this does not show
- Three calculators on one site. I ran the functions directly and checked the page itself for the mulch calculator only.
- The exact reference needs whole-number areas and percentages. Beyond that I tried five areas around 100.
- Whether the numbers are right for real materials. Density and bag size are inputs, and I only checked that the copy matches the code.
- Anything about how search engines treat the markup.
How do you keep numbers in page copy honest: generate the text from the code, or test it? I would like to hear what has worked.
The page with the 110 is the paver calculator.
This article was drafted with help from an AI assistant and edited by me. The code excerpts come from the project's repository, and the measurements are from my own runs on 2026-11-02.