368 lines
9.1 KiB
TypeScript
368 lines
9.1 KiB
TypeScript
import type {
|
|
DistributedVariableTerm,
|
|
VariableParenthesisProblem,
|
|
VariableParenthesisTerm,
|
|
VariableTermChoice,
|
|
VariableUserAnswer,
|
|
VariableGradeResult,
|
|
} from "./variable-types";
|
|
|
|
const MIN_MULTIPLIER = -9;
|
|
const MAX_MULTIPLIER = -1;
|
|
const MIN_COEFFICIENT = 1;
|
|
const MAX_COEFFICIENT = 9;
|
|
const MAX_TERM_COUNT = 4;
|
|
|
|
export type VariableParenthesisTermCount = 2 | 3 | 4;
|
|
|
|
interface VariableProblemSeed {
|
|
multiplier: number;
|
|
terms: VariableParenthesisTerm[];
|
|
operators: ("+" | "-")[];
|
|
}
|
|
|
|
function randomInt(min: number, max: number) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
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 createConstantTerm(coefficient: number): VariableParenthesisTerm {
|
|
return {
|
|
kind: "constant",
|
|
coefficient,
|
|
};
|
|
}
|
|
|
|
function createVariableTerm(coefficient: number): VariableParenthesisTerm {
|
|
return {
|
|
kind: "variable",
|
|
coefficient,
|
|
variable: "x",
|
|
};
|
|
}
|
|
|
|
function createUniqueConstantTerms(
|
|
baseCoefficient: number,
|
|
count: number,
|
|
): VariableParenthesisTerm[] {
|
|
const used = new Set<number>();
|
|
const terms: VariableParenthesisTerm[] = [];
|
|
let candidate = baseCoefficient;
|
|
|
|
while (terms.length < count) {
|
|
const coef = ((candidate - 1) % MAX_COEFFICIENT) + 1;
|
|
if (!used.has(coef)) {
|
|
used.add(coef);
|
|
terms.push(createConstantTerm(coef));
|
|
}
|
|
candidate += 1;
|
|
}
|
|
|
|
return terms;
|
|
}
|
|
|
|
function termKey(term: VariableParenthesisTerm) {
|
|
return `${term.kind}:${term.coefficient}`;
|
|
}
|
|
|
|
export function formatVariableTermLatex(
|
|
term: Pick<VariableParenthesisTerm, "kind" | "coefficient" | "variable">,
|
|
) {
|
|
if (term.kind === "constant") {
|
|
return `${term.coefficient}`;
|
|
}
|
|
|
|
if (term.coefficient === 1) {
|
|
return "x";
|
|
}
|
|
|
|
if (term.coefficient === -1) {
|
|
return "-x";
|
|
}
|
|
|
|
return `${term.coefficient}x`;
|
|
}
|
|
|
|
function formatParenthesisTermLatex(term: VariableParenthesisTerm) {
|
|
return formatVariableTermLatex(term);
|
|
}
|
|
|
|
export function formatVariableExpressionLatex(
|
|
terms: DistributedVariableTerm[],
|
|
) {
|
|
return terms
|
|
.map((term, index) => {
|
|
if (index === 0) return term.latex;
|
|
|
|
const operator = term.coefficient < 0 ? "-" : "+";
|
|
const positiveTerm = {
|
|
...term,
|
|
coefficient: Math.abs(term.coefficient),
|
|
};
|
|
|
|
return `${operator} ${formatVariableTermLatex(positiveTerm)}`;
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
function distributeTerm(
|
|
multiplier: number,
|
|
term: VariableParenthesisTerm,
|
|
sign: 1 | -1,
|
|
): DistributedVariableTerm {
|
|
const coefficient = multiplier * sign * term.coefficient;
|
|
|
|
return {
|
|
kind: term.kind,
|
|
coefficient,
|
|
variable: term.variable,
|
|
latex: formatVariableTermLatex({ ...term, coefficient }),
|
|
};
|
|
}
|
|
|
|
function createChoice(
|
|
key: string,
|
|
termIndex: number,
|
|
term: DistributedVariableTerm,
|
|
coefficient: number,
|
|
): VariableTermChoice {
|
|
const signKey = coefficient < 0 ? "negative" : "positive";
|
|
|
|
return {
|
|
id: `${key}:term-${termIndex}:${signKey}`,
|
|
coefficient,
|
|
variable: term.variable,
|
|
latex: formatVariableTermLatex({ ...term, coefficient }),
|
|
};
|
|
}
|
|
|
|
function createChoices(
|
|
key: string,
|
|
termIndex: number,
|
|
term: DistributedVariableTerm,
|
|
) {
|
|
const absoluteCoefficient = Math.abs(term.coefficient);
|
|
const choices: VariableTermChoice[] = [
|
|
createChoice(key, termIndex, term, -absoluteCoefficient),
|
|
createChoice(key, termIndex, term, absoluteCoefficient),
|
|
];
|
|
|
|
return shuffle(choices);
|
|
}
|
|
|
|
function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem {
|
|
const { multiplier, terms, operators } = seed;
|
|
const [firstTerm, secondTerm] = terms;
|
|
const [operator] = operators;
|
|
const key = [
|
|
multiplier,
|
|
...terms.flatMap((term, index) =>
|
|
index === 0 ? [termKey(term)] : [operators[index - 1], termKey(term)],
|
|
),
|
|
].join(":");
|
|
const correctTerms = terms.map((term, index) =>
|
|
distributeTerm(
|
|
multiplier,
|
|
term,
|
|
index === 0 || operators[index - 1] === "+" ? 1 : -1,
|
|
),
|
|
);
|
|
const choices = correctTerms.map((term, index) =>
|
|
createChoices(key, index, term),
|
|
);
|
|
|
|
return {
|
|
id: `variable-parenthesis:${key}`,
|
|
key,
|
|
multiplier,
|
|
terms,
|
|
operators,
|
|
operator,
|
|
firstTerm,
|
|
secondTerm,
|
|
correctTerms,
|
|
choices,
|
|
latexBefore: `${multiplier}\\left(${terms
|
|
.map((term, index) => {
|
|
if (index === 0) return formatParenthesisTermLatex(term);
|
|
|
|
return `${operators[index - 1]} ${formatParenthesisTermLatex(term)}`;
|
|
})
|
|
.join(" ")}\\right)`,
|
|
latexAfter: formatVariableExpressionLatex(correctTerms),
|
|
};
|
|
}
|
|
|
|
function createOperatorCombinations(termCount: VariableParenthesisTermCount) {
|
|
const combinations: ("+" | "-")[][] = [];
|
|
|
|
function addOperators(nextOperators: ("+" | "-")[]) {
|
|
if (nextOperators.length === termCount - 1) {
|
|
combinations.push(nextOperators);
|
|
return;
|
|
}
|
|
|
|
addOperators([...nextOperators, "+"]);
|
|
addOperators([...nextOperators, "-"]);
|
|
}
|
|
|
|
addOperators([]);
|
|
|
|
return combinations;
|
|
}
|
|
|
|
function createAllProblemSeeds(termCount: VariableParenthesisTermCount) {
|
|
const seeds: VariableProblemSeed[] = [];
|
|
const operatorCombinations = createOperatorCombinations(termCount);
|
|
|
|
for (
|
|
let multiplier = MIN_MULTIPLIER;
|
|
multiplier <= MAX_MULTIPLIER;
|
|
multiplier += 1
|
|
) {
|
|
for (
|
|
let constant = MIN_COEFFICIENT;
|
|
constant <= MAX_COEFFICIENT;
|
|
constant += 1
|
|
) {
|
|
for (
|
|
let variableCoefficient = MIN_COEFFICIENT;
|
|
variableCoefficient <= MAX_COEFFICIENT;
|
|
variableCoefficient += 1
|
|
) {
|
|
for (let variableIndex = 0; variableIndex < termCount; variableIndex += 1) {
|
|
const constantTerms = createUniqueConstantTerms(constant, termCount - 1);
|
|
let constantTermIndex = 0;
|
|
const terms = Array.from({ length: termCount }, (_, index) =>
|
|
index === variableIndex
|
|
? createVariableTerm(variableCoefficient)
|
|
: constantTerms[constantTermIndex++],
|
|
);
|
|
|
|
for (const operators of operatorCombinations) {
|
|
seeds.push({
|
|
multiplier,
|
|
terms,
|
|
operators,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return seeds;
|
|
}
|
|
|
|
const ALL_PROBLEM_SEEDS_BY_TERM_COUNT: Record<
|
|
VariableParenthesisTermCount,
|
|
VariableProblemSeed[]
|
|
> = {
|
|
2: createAllProblemSeeds(2),
|
|
3: createAllProblemSeeds(3),
|
|
4: createAllProblemSeeds(4),
|
|
};
|
|
|
|
function assertTermCount(
|
|
termCount: number,
|
|
): asserts termCount is VariableParenthesisTermCount {
|
|
if (!Number.isInteger(termCount) || termCount < 2 || termCount > MAX_TERM_COUNT) {
|
|
throw new Error("termCount must be 2, 3, or 4.");
|
|
}
|
|
}
|
|
|
|
export function generateVariableParenthesisProblem(
|
|
termCount: VariableParenthesisTermCount = 2,
|
|
): VariableParenthesisProblem {
|
|
assertTermCount(termCount);
|
|
|
|
const constant = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
|
|
const variableCoefficient = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
|
|
const variableIndex = randomInt(0, termCount - 1);
|
|
|
|
const constantTerms = createUniqueConstantTerms(constant, termCount - 1);
|
|
let constantTermIndex = 0;
|
|
|
|
return createProblem({
|
|
multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER),
|
|
terms: Array.from({ length: termCount }, (_, index) =>
|
|
index === variableIndex
|
|
? createVariableTerm(variableCoefficient)
|
|
: constantTerms[constantTermIndex++],
|
|
),
|
|
operators: Array.from({ length: termCount - 1 }, () =>
|
|
Math.random() < 0.5 ? "+" : "-",
|
|
),
|
|
});
|
|
}
|
|
|
|
export function generateVariableParenthesisSession(
|
|
count = 10,
|
|
termCount: VariableParenthesisTermCount = 2,
|
|
): VariableParenthesisProblem[] {
|
|
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);
|
|
}
|
|
|
|
function findSelectedChoice(
|
|
problem: VariableParenthesisProblem,
|
|
termIndex: number,
|
|
choiceId: string | null,
|
|
): DistributedVariableTerm | null {
|
|
if (!choiceId) return null;
|
|
|
|
const choice = problem.choices[termIndex].find((item) => item.id === choiceId);
|
|
|
|
if (!choice) return null;
|
|
|
|
return {
|
|
kind: problem.correctTerms[termIndex].kind,
|
|
coefficient: choice.coefficient,
|
|
variable: choice.variable,
|
|
latex: choice.latex,
|
|
};
|
|
}
|
|
|
|
export function gradeVariableParenthesisAnswer(
|
|
problem: VariableParenthesisProblem,
|
|
answer: VariableUserAnswer,
|
|
): VariableGradeResult {
|
|
const userAnswer = problem.correctTerms.map((_, index) =>
|
|
findSelectedChoice(problem, index, answer.selectedChoiceIds[index] ?? null),
|
|
);
|
|
const correct =
|
|
userAnswer.length === problem.correctTerms.length &&
|
|
userAnswer.every(
|
|
(term, index) => term?.coefficient === problem.correctTerms[index].coefficient,
|
|
);
|
|
|
|
return {
|
|
correct,
|
|
userAnswer,
|
|
correctAnswer: problem.correctTerms,
|
|
};
|
|
}
|