import { Fraction } from "./fractions"; import { simplifyRatio } from "./ratios"; /** Parse a strict decimal string (digits and optional single dot). Rejects scientific notation, trailing letters, etc. */ export function parseStrictDecimal(input: string): number | null { const s = input.trim(); if (!/^\d+(\.\d+)?$/.test(s)) return null; return parseFloat(s); } /** Parse a strict integer string (digits only, no dot or letters). */ export function parseStrictInt(input: string): number | null { const s = input.trim(); if (!/^\d+$/.test(s)) return null; return parseInt(s, 10); } /** Parse a strict signed integer string. Reject decimals, scientific notation, and trailing text. */ export function parseStrictSignedInt(input: string): number | null { const s = input.trim(); if (!/^-?\d+$/.test(s)) return null; return parseInt(s, 10); } /** Parse a strict signed decimal string. Reject scientific notation and trailing text. */ export function parseStrictSignedDecimal(input: string): number | null { const s = input.trim(); if (!/^-?\d+(\.\d+)?$/.test(s)) return null; return parseFloat(s); } /** * The outcome of checking one submitted answer. * - `solved` — correct and in the required form (fully done) * - `unsimplified` — correct value, but still needs simplifying (a nudge, not done) * - `wrong` — incorrect; `message` explains why * * A single `status` field means callers branch on one thing, instead of an * error-prone `correct`/`simplified` boolean pair. */ export type AnswerResult = | { status: "solved" } | { status: "unsimplified" } | { status: "wrong"; message: string }; export const SOLVED: AnswerResult = { status: "solved" }; export const UNSIMPLIFIED: AnswerResult = { status: "unsimplified" }; export function wrong(message: string): AnswerResult { return { status: "wrong", message }; } export function checkFractionAnswer( userNum: number, userDen: number, expectedNum: number, expectedDen: number, requireSimplified: boolean = true, ): AnswerResult { if (userDen === 0) { return wrong("Denominator cannot be zero"); } const user = new Fraction(userNum, userDen); const expected = new Fraction(expectedNum, expectedDen); if (!user.equals(expected)) { return wrong("That's not quite right. Try again!"); } // Any non-zero denominator gives the same value for zero, so don't force 0/1 in the UI. if (user.isZero) { return SOLVED; } if (requireSimplified && !user.isSimplified) { return UNSIMPLIFIED; } return SOLVED; } export function checkDecimalAnswer( userValue: number, expectedValue: number, tolerance: number = 0.001, ): AnswerResult { if (Math.abs(userValue - expectedValue) <= tolerance) { return SOLVED; } return wrong("That's not quite right. Try again!"); } export function checkRatioAnswer( userParts: number[], expectedParts: number[], requireSimplified: boolean = true, ): AnswerResult { if (userParts.length !== expectedParts.length) { return wrong("Check the number of parts in your ratio"); } const userSimplified = simplifyRatio(userParts); const expectedSimplified = simplifyRatio(expectedParts); const isEquivalent = userSimplified.every((v, i) => v === expectedSimplified[i]); if (!isEquivalent) { return wrong("That's not quite right. Try again!"); } if (requireSimplified) { const isAlreadySimplified = userParts.every((v, i) => v === userSimplified[i]); return isAlreadySimplified ? SOLVED : UNSIMPLIFIED; } return SOLVED; } export function checkIntegerAnswer( userValue: number, expectedValue: number, ): AnswerResult { return userValue === expectedValue ? SOLVED : wrong("That's not quite right. Try again!"); } export function checkOrderingAnswer( userOrder: number[], expectedOrder: number[], ): AnswerResult { if (userOrder.length !== expectedOrder.length) { return wrong("Make sure you've ordered all the numbers"); } const isCorrect = userOrder.every((v, i) => v === expectedOrder[i]); return isCorrect ? SOLVED : wrong("Check your ordering. Try again!"); }