Joel Stevick

Appendix B · section 30 of 32

ask/register.ts

From Measuring the Whole Thing.

// Whether a shown answer reads the way a sixteen-year-old wants one: a
// couple of sentences, boiled down, with no law's number in them. The eval
// scores every case by it beside the facts, and a miss is a miss.

/** A question of the consequence shape -- what happens, what if, will I, do I have to, how much -- may take three sentences; every other question takes two. */
export function consequenceShaped(question: string): boolean {
  return /\b(what happens|what if|will i|do i have to|how much)\b/i.test(question);
}

// A sentence ends at a full stop, a question mark or an exclamation mark
// followed by a space and a capital, or at the end of the text: "5 a.m. and"
// and "0.08" are not two sentences, and nor is an initialism before a name.
function sentencesOf(text: string): string[] {
  return text
    .trim()
    .split(/(?<!\b(?:\p{Lu}\.){2,})(?<=[.!?])["”’)]?\s+(?=["“¿¡(]?[\p{Lu}\d$])/u)
    .filter((sentence) => /\p{L}/u.test(sentence));
}

export function sentencesIn(text: string): number {
  return sentencesOf(text).length;
}

/** The longest a sentence may run, in words. The prompt asks for under about 20; 28 is where a sentence is a miss. */
export const MAX_SENTENCE_WORDS = 28;

export function longestSentenceWords(text: string): number {
  return Math.max(
    0,
    ...sentencesOf(text).map((sentence) => sentence.split(/\s+/).filter((word) => /[\p{L}\d]/u.test(word)).length),
  );
}

/** A law's name or number written into the sentences, which the app already shows under them: "§ 16-112", "p.30", "page 30", "section 21-1003". */
export function namesALaw(text: string): boolean {
  return (
    /§|\bp\.\s?\d|\bpages?\b|\b(sub)?sections?\s+\(?\d|\bsubsection\b/i.test(text) ||
    /(?<![$\d.,])\b\d{2,}[A-Z]{0,2}[.:-]\d+(?![\d.]*\s*(?:[ap]\.?m\b|dollars?|days?|feet|miles?|mph|hours?|years?|months?|percent|points?))/i.test(text)
  );
}

/** What is wrong with a shown answer's register, or nothing. An answer that was not shown has no register to judge. */
export function registerMisses(found: { question: string; shown: string }): string[] {
  if (found.shown.trim() === "") return [];
  const out: string[] = [];
  const most = consequenceShaped(found.question) ? 3 : 2;
  const count = sentencesIn(found.shown);
  if (count > most) out.push(`register: ${count} sentences, at most ${most}`);
  const longest = longestSentenceWords(found.shown);
  if (longest > MAX_SENTENCE_WORDS) out.push(`register: a sentence of ${longest} words, at most ${MAX_SENTENCE_WORDS}`);
  if (namesALaw(found.shown)) out.push("register: names a law or page in the sentences");
  return out;
}