Files
math-quiz/lib/engine/parenthesis-sign.ts
T

215 lines
5.0 KiB
TypeScript

import type {
GradeResult,
ParenthesisSignProblem,
UserAnswer,
} from "./types";
import { formatParenthesisExpressionLatex } from "./parenthesis-latex";
const MIN_MULTIPLIER = -9;
const MAX_MULTIPLIER = -1;
const MIN_TERM = 1;
const MAX_TERM = 9;
const MAX_TERM_COUNT = 4;
export type ParenthesisTermCount = 2 | 3 | 4;
interface ProblemSeed {
multiplier: number;
terms: number[];
operators: ("+" | "-")[];
}
function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function formatLatexTerms(terms: number[]) {
return terms
.map((term, index) => {
if (index === 0) return `${term}`;
const operator = term < 0 ? "-" : "+";
return `${operator} ${Math.abs(term)}`;
})
.join(" ");
}
function createProblem({
multiplier,
terms,
operators,
}: ProblemSeed): ParenthesisSignProblem {
const signedTerms = terms.map((term, index) => {
if (index === 0) return term;
return operators[index - 1] === "-" ? -term : term;
});
const distributedTerms = signedTerms.map((term) => multiplier * term);
const key = [
multiplier,
...terms.flatMap((term, index) =>
index === 0 ? [`${term}`] : [operators[index - 1], `${term}`],
),
].join(":");
return {
id: `parenthesis-sign:${key}`,
key,
multiplier,
terms,
operators,
distributedTerms,
latexBefore: formatParenthesisExpressionLatex({
multiplier,
terms,
operators,
formatTermLatex: (term) => `${term}`,
}),
latexAfter: formatLatexTerms(distributedTerms),
};
}
function createTermCombinations(termCount: ParenthesisTermCount) {
const combinations: number[][] = [];
function addTerms(nextTerms: number[]) {
if (nextTerms.length === termCount) {
combinations.push(nextTerms);
return;
}
for (let term = MIN_TERM; term <= MAX_TERM; term += 1) {
if (nextTerms.includes(term)) continue;
addTerms([...nextTerms, term]);
}
}
addTerms([]);
return combinations;
}
function createOperatorCombinations(termCount: ParenthesisTermCount) {
const combinations: ("+" | "-")[][] = [];
const operatorCount = termCount - 1;
function addOperators(nextOperators: ("+" | "-")[]) {
if (nextOperators.length === operatorCount) {
combinations.push(nextOperators);
return;
}
addOperators([...nextOperators, "+"]);
addOperators([...nextOperators, "-"]);
}
addOperators([]);
return combinations;
}
function createAllProblemSeeds(termCount: ParenthesisTermCount) {
const seeds: ProblemSeed[] = [];
const termCombinations = createTermCombinations(termCount);
const operatorCombinations = createOperatorCombinations(termCount);
for (
let multiplier = MIN_MULTIPLIER;
multiplier <= MAX_MULTIPLIER;
multiplier += 1
) {
for (const terms of termCombinations) {
for (const operators of operatorCombinations) {
seeds.push({ multiplier, terms, operators });
}
}
}
return seeds;
}
const ALL_PROBLEM_SEEDS_BY_TERM_COUNT: Record<
ParenthesisTermCount,
ProblemSeed[]
> = {
2: createAllProblemSeeds(2),
3: createAllProblemSeeds(3),
4: createAllProblemSeeds(4),
};
function shuffle<T>(items: T[]) {
const shuffled = [...items];
for (let index = shuffled.length - 1; index > 0; index -= 1) {
const targetIndex = randomInt(0, index);
const current = shuffled[index];
shuffled[index] = shuffled[targetIndex];
shuffled[targetIndex] = current;
}
return shuffled;
}
function assertTermCount(termCount: number): asserts termCount is ParenthesisTermCount {
if (!Number.isInteger(termCount) || termCount < 2 || termCount > MAX_TERM_COUNT) {
throw new Error("termCount must be 2, 3, or 4.");
}
}
export function generateProblem(
termCount: ParenthesisTermCount = 2,
): ParenthesisSignProblem {
assertTermCount(termCount);
const terms: number[] = [];
while (terms.length < termCount) {
const term = randomInt(MIN_TERM, MAX_TERM);
if (!terms.includes(term)) {
terms.push(term);
}
}
return createProblem({
multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER),
terms,
operators: Array.from({ length: termCount - 1 }, () =>
Math.random() < 0.5 ? "+" : "-",
),
});
}
export function generateSession(
count = 10,
termCount: ParenthesisTermCount = 2,
): ParenthesisSignProblem[] {
if (!Number.isInteger(count) || count < 1) {
throw new Error("count must be a positive integer.");
}
assertTermCount(termCount);
const seeds = ALL_PROBLEM_SEEDS_BY_TERM_COUNT[termCount];
if (count > seeds.length) {
throw new Error(`count cannot exceed ${seeds.length}.`);
}
return shuffle(seeds).slice(0, count).map(createProblem);
}
export function gradeAnswer(
problem: ParenthesisSignProblem,
answer: UserAnswer,
): GradeResult {
return {
correct:
answer.terms.length === problem.distributedTerms.length &&
answer.terms.every((value, index) => value === problem.distributedTerms[index]),
userAnswer: answer.terms,
correctAnswer: problem.distributedTerms,
};
}