If you build a calculator that turns a raw result into points, the scoring table is the product. A bug in the arithmetic is easy to spot. A single wrong row in a 100-line table is not, and users will trust it anyway. This post shows a small way to protect a lookup-table calculator: keep the table as data, keep a second copy of the source numbers as a "golden file," and test every row automatically.

The numbers in the examples below are made up for illustration. When you build one for a real standard, copy the values from the governing document and cite it on the page.

Step 1: keep the table as data, not code

Hard-coding thresholds inside if statements makes them impossible to review. Put them in a plain array instead, sorted from best to worst:

// pushups.table.js (illustrative values only) export const pushupTable = [ { min: 60, points: 100 }, { min: 50, points: 90 }, { min: 40, points: 80 }, { min: 30, points: 70 }, { min: 20, points: 60 }, { min: 0, points: 0 } ]; 

A reviewer can now compare this file to the source document line by line, without reading any logic.

Step 2: one tiny scoring function

For events where more is better, the score is the first row whose threshold you meet:

export function scoreHigherIsBetter(table, value) { for (const row of table) { if (value >= row.min) return row.points; } return 0; } 

For timed events where less is better (a run, for example), flip the comparison and store max seconds instead of min. Store times as whole seconds, never as strings like "14:30", so you never parse text at scoring time.

export function scoreLowerIsBetter(table, seconds) { for (const row of table) { if (seconds <= row.max) return row.points; } return 0; } 

Step 3: a golden file

Create a second file by hand from the source, in a different shape, ideally a CSV you typed while looking at the document rather than the code:

event,value,expected_points pushups,60,100 pushups,59,90 pushups,50,90 pushups,49,80 pushups,20,60 pushups,19,0 

Notice what these rows test: each threshold, and the value one step below it. Boundaries are where tables go wrong, because "50 or more" and "more than 50" are different tables that look alike.

Step 4: test every row

import { readFileSync } from "node:fs"; import test from "node:test"; import assert from "node:assert/strict"; import { pushupTable } from "./pushups.table.js"; import { scoreHigherIsBetter } from "./score.js"; const rows = readFileSync("golden.csv", "utf8") .trim().split("\n").slice(1) .map(line => line.split(",")); for (const [event, value, expected] of rows) { if (event !== "pushups") continue; test(`pushups ${value} -> ${expected}`, () => { assert.equal(scoreHigherIsBetter(pushupTable, Number(value)), Number(expected)); }); } 

Run it with node --test. Because the golden file was typed separately from the table, a typo has to happen twice, in the same place, to survive.

Step 5: test the shape of the table too

Some bugs are not wrong values but a broken table. Add checks that catch them before any user does:

test("table is sorted best to worst", () => { for (let i = 1; i < pushupTable.length; i++) { assert.ok(pushupTable[i - 1].min > pushupTable[i].min); assert.ok(pushupTable[i - 1].points >= pushupTable[i].points); } }); test("table ends with a zero floor", () => { assert.equal(pushupTable.at(-1).min, 0); }); 

If someone later pastes a new row in the wrong place, the build fails with a clear message.

Step 6: record where the numbers came from

Add a source field next to each table with the document name, the edition date, and the page. Show it on the page next to the result. When a standard changes, you will know exactly which tables are stale, and your users will know which edition they are looking at. That visible source date is the cheapest trust signal a calculator can have.

I use this approach for the military fitness calculators at AFTPTCalc (https://aftptcalc.com/methodology/ explains how the tables are checked). It is an independent, unofficial project, so the point of the golden file is simply to make sure the page says what the published scale says.

What this catches in practice

  • An off-by-one at a threshold ("50 and up" typed as "51 and up").
  • Two rows swapped while pasting.
  • A time converted to seconds incorrectly (14:30 is 870 seconds, not 850).
  • A table edited for one age group that accidentally changed another.

None of this needs a framework. It needs a table that is data, a second copy typed independently, and a test per row. For a calculator people use to plan their training, that is a small price for being right.

Disclosure: I used an AI writing assistant to help draft and edit this post. The code and examples are my own and the numbers are illustrative.