Files
cabrits-math/lib/math/validation.ts
2026-07-05 11:12:45 -04:00

135 lines
4.0 KiB
TypeScript

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!");
}