미지수 괄호 문제 풀이 추가
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
formatVariableExpressionLatex,
|
||||
generateVariableParenthesisProblem,
|
||||
generateVariableParenthesisSession,
|
||||
gradeVariableParenthesisAnswer,
|
||||
} from "./variable-parenthesis";
|
||||
import type { VariableParenthesisProblem } from "./variable-types";
|
||||
|
||||
function findCorrectChoiceIds(problem: VariableParenthesisProblem) {
|
||||
return problem.correctTerms.map((term, index) => {
|
||||
const choices = problem.choices[index as 0 | 1];
|
||||
const choice = choices.find(
|
||||
(item) =>
|
||||
item.coefficient === term.coefficient && item.variable === term.variable,
|
||||
);
|
||||
|
||||
if (!choice) throw new Error("correct choice not found.");
|
||||
|
||||
return choice.id;
|
||||
}) as [string, string];
|
||||
}
|
||||
|
||||
describe("variable parenthesis engine", () => {
|
||||
it("generates a problem with exactly one variable term inside parentheses", () => {
|
||||
const problem = generateVariableParenthesisProblem();
|
||||
const terms = [problem.firstTerm, problem.secondTerm];
|
||||
|
||||
expect(terms.filter((term) => term.kind === "variable")).toHaveLength(1);
|
||||
expect(terms.filter((term) => term.kind === "constant")).toHaveLength(1);
|
||||
expect(problem.multiplier).toBeLessThan(0);
|
||||
expect(Number.isInteger(problem.multiplier)).toBe(true);
|
||||
expect(problem.firstTerm.coefficient).toBeGreaterThanOrEqual(1);
|
||||
expect(problem.secondTerm.coefficient).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("distributes signs correctly for addition and subtraction", () => {
|
||||
const problems = generateVariableParenthesisSession(50);
|
||||
|
||||
for (const problem of problems) {
|
||||
const secondSign = problem.operator === "-" ? -1 : 1;
|
||||
|
||||
expect(problem.correctTerms[0].coefficient).toBe(
|
||||
problem.multiplier * problem.firstTerm.coefficient,
|
||||
);
|
||||
expect(problem.correctTerms[1].coefficient).toBe(
|
||||
problem.multiplier * secondSign * problem.secondTerm.coefficient,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("creates positive and negative choices for each term", () => {
|
||||
const problem = generateVariableParenthesisProblem();
|
||||
|
||||
problem.choices.forEach((choices, index) => {
|
||||
const correctTerm = problem.correctTerms[index as 0 | 1];
|
||||
const coefficients = choices.map((choice) => choice.coefficient).sort();
|
||||
|
||||
expect(choices).toHaveLength(2);
|
||||
expect(coefficients).toEqual([
|
||||
-Math.abs(correctTerm.coefficient),
|
||||
Math.abs(correctTerm.coefficient),
|
||||
]);
|
||||
expect(choices.every((choice) => choice.variable === correctTerm.variable))
|
||||
.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("grades selected choice ids", () => {
|
||||
const problem = generateVariableParenthesisProblem();
|
||||
const correctChoiceIds = findCorrectChoiceIds(problem);
|
||||
const result = gradeVariableParenthesisAnswer(problem, {
|
||||
selectedChoiceIds: correctChoiceIds,
|
||||
});
|
||||
|
||||
expect(result.correct).toBe(true);
|
||||
expect(result.correctAnswer).toEqual(problem.correctTerms);
|
||||
|
||||
const wrongFirstChoice = problem.choices[0].find(
|
||||
(choice) => choice.id !== correctChoiceIds[0],
|
||||
);
|
||||
|
||||
expect(wrongFirstChoice).toBeDefined();
|
||||
expect(
|
||||
gradeVariableParenthesisAnswer(problem, {
|
||||
selectedChoiceIds: [wrongFirstChoice!.id, correctChoiceIds[1]],
|
||||
}).correct,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("generates sessions without duplicate keys", () => {
|
||||
const problems = generateVariableParenthesisSession(10);
|
||||
const keys = new Set(problems.map((problem) => problem.key));
|
||||
|
||||
expect(problems).toHaveLength(10);
|
||||
expect(keys.size).toBe(10);
|
||||
});
|
||||
|
||||
it("formats expressions with variable terms", () => {
|
||||
expect(
|
||||
formatVariableExpressionLatex([
|
||||
{ kind: "variable", coefficient: -6, variable: "x", latex: "-6x" },
|
||||
{ kind: "constant", coefficient: 12, latex: "12" },
|
||||
]),
|
||||
).toBe("-6x + 12");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
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_PROBLEM_COUNT = 9 * 9 * 9 * 2 * 2;
|
||||
|
||||
interface VariableProblemSeed {
|
||||
multiplier: number;
|
||||
operator: "+" | "-";
|
||||
firstTerm: VariableParenthesisTerm;
|
||||
secondTerm: VariableParenthesisTerm;
|
||||
}
|
||||
|
||||
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 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, DistributedVariableTerm],
|
||||
) {
|
||||
const [firstTerm, secondTerm] = terms;
|
||||
const operator = secondTerm.coefficient < 0 ? "-" : "+";
|
||||
const positiveSecondTerm = {
|
||||
...secondTerm,
|
||||
coefficient: Math.abs(secondTerm.coefficient),
|
||||
};
|
||||
|
||||
return `${firstTerm.latex} ${operator} ${formatVariableTermLatex(
|
||||
positiveSecondTerm,
|
||||
)}`;
|
||||
}
|
||||
|
||||
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: 0 | 1,
|
||||
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: 0 | 1,
|
||||
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, operator, firstTerm, secondTerm } = seed;
|
||||
const key = [
|
||||
multiplier,
|
||||
termKey(firstTerm),
|
||||
operator,
|
||||
termKey(secondTerm),
|
||||
].join(":");
|
||||
const correctTerms: [DistributedVariableTerm, DistributedVariableTerm] = [
|
||||
distributeTerm(multiplier, firstTerm, 1),
|
||||
distributeTerm(multiplier, secondTerm, operator === "-" ? -1 : 1),
|
||||
];
|
||||
const choices: [VariableTermChoice[], VariableTermChoice[]] = [
|
||||
createChoices(key, 0, correctTerms[0]),
|
||||
createChoices(key, 1, correctTerms[1]),
|
||||
];
|
||||
|
||||
return {
|
||||
id: `variable-parenthesis:${key}`,
|
||||
key,
|
||||
multiplier,
|
||||
operator,
|
||||
firstTerm,
|
||||
secondTerm,
|
||||
correctTerms,
|
||||
choices,
|
||||
latexBefore: `${multiplier}\\left(${formatParenthesisTermLatex(
|
||||
firstTerm,
|
||||
)} ${operator} ${formatParenthesisTermLatex(secondTerm)}\\right)`,
|
||||
latexAfter: formatVariableExpressionLatex(correctTerms),
|
||||
};
|
||||
}
|
||||
|
||||
function createAllProblemSeeds() {
|
||||
const seeds: VariableProblemSeed[] = [];
|
||||
|
||||
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 (const operator of ["+", "-"] as const) {
|
||||
seeds.push({
|
||||
multiplier,
|
||||
operator,
|
||||
firstTerm: createVariableTerm(variableCoefficient),
|
||||
secondTerm: createConstantTerm(constant),
|
||||
});
|
||||
seeds.push({
|
||||
multiplier,
|
||||
operator,
|
||||
firstTerm: createConstantTerm(constant),
|
||||
secondTerm: createVariableTerm(variableCoefficient),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return seeds;
|
||||
}
|
||||
|
||||
const ALL_PROBLEM_SEEDS = createAllProblemSeeds();
|
||||
|
||||
export function generateVariableParenthesisProblem(): VariableParenthesisProblem {
|
||||
const constant = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
|
||||
const variableCoefficient = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
|
||||
const variableFirst = Math.random() < 0.5;
|
||||
const operator = Math.random() < 0.5 ? "+" : "-";
|
||||
|
||||
return createProblem({
|
||||
multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER),
|
||||
operator,
|
||||
firstTerm: variableFirst
|
||||
? createVariableTerm(variableCoefficient)
|
||||
: createConstantTerm(constant),
|
||||
secondTerm: variableFirst
|
||||
? createConstantTerm(constant)
|
||||
: createVariableTerm(variableCoefficient),
|
||||
});
|
||||
}
|
||||
|
||||
export function generateVariableParenthesisSession(
|
||||
count = 10,
|
||||
): VariableParenthesisProblem[] {
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
throw new Error("count must be a positive integer.");
|
||||
}
|
||||
|
||||
if (count > MAX_PROBLEM_COUNT) {
|
||||
throw new Error(`count cannot exceed ${MAX_PROBLEM_COUNT}.`);
|
||||
}
|
||||
|
||||
return shuffle(ALL_PROBLEM_SEEDS).slice(0, count).map(createProblem);
|
||||
}
|
||||
|
||||
function findSelectedChoice(
|
||||
problem: VariableParenthesisProblem,
|
||||
termIndex: 0 | 1,
|
||||
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: [
|
||||
DistributedVariableTerm | null,
|
||||
DistributedVariableTerm | null,
|
||||
] = [
|
||||
findSelectedChoice(problem, 0, answer.selectedChoiceIds[0]),
|
||||
findSelectedChoice(problem, 1, answer.selectedChoiceIds[1]),
|
||||
];
|
||||
const correct =
|
||||
userAnswer[0]?.coefficient === problem.correctTerms[0].coefficient &&
|
||||
userAnswer[1]?.coefficient === problem.correctTerms[1].coefficient;
|
||||
|
||||
return {
|
||||
correct,
|
||||
userAnswer,
|
||||
correctAnswer: problem.correctTerms,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type VariableTermKind = "constant" | "variable";
|
||||
|
||||
export interface VariableParenthesisTerm {
|
||||
kind: VariableTermKind;
|
||||
coefficient: number;
|
||||
variable?: "x";
|
||||
}
|
||||
|
||||
export interface DistributedVariableTerm {
|
||||
kind: VariableTermKind;
|
||||
coefficient: number;
|
||||
variable?: "x";
|
||||
latex: string;
|
||||
}
|
||||
|
||||
export interface VariableTermChoice {
|
||||
id: string;
|
||||
coefficient: number;
|
||||
variable?: "x";
|
||||
latex: string;
|
||||
}
|
||||
|
||||
export interface VariableParenthesisProblem {
|
||||
id: string;
|
||||
key: string;
|
||||
multiplier: number;
|
||||
operator: "+" | "-";
|
||||
firstTerm: VariableParenthesisTerm;
|
||||
secondTerm: VariableParenthesisTerm;
|
||||
correctTerms: [DistributedVariableTerm, DistributedVariableTerm];
|
||||
choices: [VariableTermChoice[], VariableTermChoice[]];
|
||||
latexBefore: string;
|
||||
latexAfter: string;
|
||||
}
|
||||
|
||||
export interface VariableUserAnswer {
|
||||
selectedChoiceIds: [string | null, string | null];
|
||||
}
|
||||
|
||||
export interface VariableGradeResult {
|
||||
correct: boolean;
|
||||
userAnswer: [DistributedVariableTerm | null, DistributedVariableTerm | null];
|
||||
correctAnswer: [DistributedVariableTerm, DistributedVariableTerm];
|
||||
}
|
||||
Reference in New Issue
Block a user