Appendix B · section 10 of 32
ask/figures.ts
From The Check That Is Not the Model.
// Every figure a text states, as strings of digits: its digit runs and its
// spelled numbers. Statutes spell their figures where manuals print digits,
// and the prompt asks the model for digits, so "closer than five hundred feet"
// in a law has to ground "500 feet" in an answer.
const SMALL = [
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve",
];
const UNITS: Record<string, number> = {
one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8,
nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14,
fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19,
};
const TENS: Record<string, number> = {
twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70,
eighty: 80, ninety: 90,
};
export function figuresIn(text: string): Set<string> {
// "1,000" is one figure, not 1 and 000. "0.08" is read as ".08", the way
// the manuals write a blood alcohol level, so the leading zero is not a
// figure of its own.
const normalised = text
.replace(/(\d),(?=\d{3}(?!\d))/g, "$1")
.replace(/\b0(\.\d)/g, "$1");
const found = new Set<string>(normalised.match(/\d+/g) ?? []);
const lower = normalised.toLowerCase();
SMALL.forEach((word, value) => {
if (new RegExp(`\\b${word}\\b`).test(lower)) found.add(String(value));
});
for (const word of lower.match(/[a-z]+/g) ?? []) {
if (UNITS[word] !== undefined) found.add(String(UNITS[word]));
if (TENS[word] !== undefined) found.add(String(TENS[word]));
if (word === "hundred") found.add("100");
if (word === "thousand") found.add("1000");
}
for (const value of compounds(lower)) found.add(String(value));
// "One" is a pronoun far more often than a figure in a road-law answer --
// "within 15 feet of one", "a one-way street" -- and read as 1 it refused
// correct answers. A digit 1 standing on its own is still a figure.
if (!/(^|[^\d.])1(?![\d])/.test(normalised)) found.delete("1");
return found;
}
// Spelled compounds as the whole numbers they are: "five hundred" is 500,
// "twenty-five" is 25, "one thousand five hundred" is 1500. Added beside the
// word-by-word values, never instead of them, so nothing that was grounded
// before stops being grounded.
export function compounds(text: string): number[] {
const out: number[] = [];
let total = 0;
let chunk = 0;
let open = false;
const close = (): void => {
if (open) out.push(total + chunk);
total = 0;
chunk = 0;
open = false;
};
for (const word of text.toLowerCase().match(/[a-z]+/g) ?? []) {
if (UNITS[word] !== undefined) chunk += UNITS[word];
else if (TENS[word] !== undefined) chunk += TENS[word];
else if (word === "hundred" && open) chunk = (chunk === 0 ? 1 : chunk) * 100;
else if (word === "thousand" && open) {
total += (chunk === 0 ? 1 : chunk) * 1000;
chunk = 0;
} else if (word === "and" && open) continue;
else {
close();
continue;
}
open = true;
}
close();
return out;
}