135 lines
4.0 KiB
TypeScript
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!");
|
|
}
|