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

337 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { MathProblem, Difficulty } from "../types";
import { randomInt, randomChoice } from "@/lib/utils";
import { Fraction, lcm } from "@/lib/math/fractions";
let counter = 0;
function nextId() {
return `fp-${++counter}-${Date.now()}`;
}
export function generateFractionAddSubtract(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["add", "subtract"] as const);
const op = operation === "add" ? "+" : "-";
let n1: number, d1: number, n2: number, d2: number;
if (difficulty === 1) {
// Common denominators
d1 = randomChoice([2, 3, 4, 5, 6, 8, 10, 12]);
d2 = d1;
n1 = randomInt(1, d1 - 1);
n2 = operation === "subtract" ? randomInt(1, n1) : randomInt(1, d1 - n1);
} else if (difficulty === 2) {
// One denominator is a multiple of the other
d1 = randomChoice([2, 3, 4, 5, 6]);
const multiplier = randomChoice([2, 3]);
d2 = d1 * multiplier;
n1 = randomInt(1, d1 - 1);
n2 = randomInt(1, d2 - 1);
} else {
// Unlike denominators
d1 = randomChoice([3, 4, 5, 7, 8, 9]);
d2 = randomChoice([3, 4, 5, 7, 8, 9].filter((d) => d !== d1));
n1 = randomInt(1, d1 - 1);
n2 = randomInt(1, d2 - 1);
}
const prompt = `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`;
const commonDen = lcm(d1, d2);
const convertedN1 = n1 * (commonDen / d1);
const convertedN2 = n2 * (commonDen / d2);
const rawNum = operation === "add"
? convertedN1 + convertedN2
: convertedN1 - convertedN2;
const { n: ansNum, d: ansDen } = new Fraction(rawNum, commonDen).simplified();
const steps = d1 === d2
? [
{
explanation: `The denominators already match, so keep ${d1} as the denominator.`,
math: `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`,
},
{
explanation: `${operation === "add" ? "Add" : "Subtract"} the numerators and keep the denominator the same.`,
math: `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}} = \\frac{${rawNum}}{${d1}}`,
},
...(ansNum !== rawNum || ansDen !== d1
? [{
explanation: "Simplify the fraction.",
math: `\\frac{${rawNum}}{${d1}} = \\frac{${ansNum}}{${ansDen}}`,
}]
: []),
]
: [
{
explanation: `Find the lowest common denominator: LCM(${d1}, ${d2}) = ${commonDen}.`,
math: `\\text{LCD} = ${commonDen}`,
},
{
explanation: "Rewrite both fractions with that denominator.",
math: `\\frac{${n1}}{${d1}} = \\frac{${convertedN1}}{${commonDen}} \\quad \\frac{${n2}}{${d2}} = \\frac{${convertedN2}}{${commonDen}}`,
},
{
explanation: `${operation === "add" ? "Add" : "Subtract"} the numerators and keep the common denominator.`,
math: `\\frac{${convertedN1}}{${commonDen}} ${op} \\frac{${convertedN2}}{${commonDen}} = \\frac{${rawNum}}{${commonDen}}`,
},
...(ansNum !== rawNum || ansDen !== commonDen
? [{
explanation: "Simplify the fraction.",
math: `\\frac{${rawNum}}{${commonDen}} = \\frac{${ansNum}}{${ansDen}}`,
}]
: []),
];
return {
id: nextId(),
prompt,
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
hints: [
d1 === d2 ? "The denominators are already the same!" : "Find a common denominator first",
d1 !== d2 ? "Rewrite both fractions using that denominator" : `Just ${operation} the numerators`,
],
steps,
};
}
export function generateFractionMultiply(difficulty: Difficulty): MathProblem {
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [2, 3, 4, 5, 6, 7, 8] : [3, 5, 7, 9, 11, 12, 15];
const d1 = randomChoice(denChoices);
const d2 = randomChoice(denChoices);
const n1 = randomInt(1, d1 - 1);
const n2 = randomInt(1, d2 - 1);
const { n: ansNum, d: ansDen } = Fraction.of(n1, d1).times(Fraction.of(n2, d2)).simplified();
return {
id: nextId(),
prompt: `\\frac{${n1}}{${d1}} \\times \\frac{${n2}}{${d2}}`,
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
hints: [
"Multiply numerator × numerator",
"Multiply denominator × denominator",
"Simplify if possible",
],
steps: [
{ explanation: `Multiply numerators: ${n1} × ${n2} = ${n1 * n2}` },
{ explanation: `Multiply denominators: ${d1} × ${d2} = ${d1 * d2}` },
{ explanation: "Simplify", math: `\\frac{${ansNum}}{${ansDen}}` },
],
};
}
export function generateFractionDivide(difficulty: Difficulty): MathProblem {
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [2, 3, 4, 5, 6, 7, 8] : [3, 5, 7, 9, 11, 14, 15];
const d1 = randomChoice(denChoices);
const d2 = randomChoice(denChoices);
const n1 = randomInt(1, d1 - 1);
const n2 = randomInt(1, d2 - 1);
const { n: ansNum, d: ansDen } = Fraction.of(n1, d1).dividedBy(Fraction.of(n2, d2)).simplified();
return {
id: nextId(),
prompt: `\\frac{${n1}}{${d1}} \\div \\frac{${n2}}{${d2}}`,
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
hints: [
"Keep the first fraction the same",
"Flip the second fraction (reciprocal)",
"Then multiply",
],
steps: [
{ explanation: `Keep ${n1}/${d1}, flip ${n2}/${d2} to ${d2}/${n2}` },
{ explanation: `Multiply: ${n1}×${d2} / ${d1}×${n2}` },
{ explanation: "Simplify", math: `\\frac{${ansNum}}{${ansDen}}` },
],
};
}
export function generateFractionOfQuantity(difficulty: Difficulty): MathProblem {
const denChoices = difficulty === 1 ? [2, 4, 5, 10] : difficulty === 2 ? [3, 4, 5, 6, 8] : [7, 8, 9, 12, 15];
const den = randomChoice(denChoices);
const num = randomInt(1, den - 1);
const multiplier = randomChoice(difficulty === 1 ? [2, 4, 5, 10] : [3, 6, 7, 8, 9, 12]);
const quantity = den * multiplier;
const answer = num * multiplier;
return {
id: nextId(),
prompt: `\\frac{${num}}{${den}} \\text{ of } ${quantity}`,
answer: { kind: "integer", value: answer },
hints: [
`Divide ${quantity} by ${den} first`,
`Then multiply by ${num}`,
],
steps: [
{ explanation: `Divide the quantity by the denominator: ${quantity} ÷ ${den} = ${quantity / den}` },
{ explanation: `Multiply by the numerator: ${quantity / den} × ${num} = ${answer}` },
],
};
}
/** A proper-fraction numerator with a random sign, magnitude 1..den-1. */
function signedProperNum(den: number): number {
const mag = randomInt(1, Math.max(1, den - 1));
return Math.random() < 0.5 ? -mag : mag;
}
/** Two proper fractions where at least one is negative (this is the negative-fractions topic). */
function signedFractionPair(d1: number, d2: number): [Fraction, Fraction] {
let n1 = signedProperNum(d1);
let n2 = signedProperNum(d2);
if (n1 > 0 && n2 > 0) {
if (Math.random() < 0.5) n1 = -n1;
else n2 = -n2;
}
return [new Fraction(n1, d1), new Fraction(n2, d2)];
}
function signedOperand(f: Fraction): string {
return f.n < 0 ? `\\left(${f.toSignedKatex()}\\right)` : f.toSignedKatex();
}
export function generateSignedFractionAddSubtract(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["+", "-"] as const);
const dens = difficulty === 1 ? [2, 3, 4] : difficulty === 2 ? [2, 3, 4, 5, 6] : [3, 4, 5, 6, 8];
const d1 = randomChoice(dens);
const d2 = randomChoice(dens);
const [a, b] = signedFractionPair(d1, d2);
const result = (operation === "+" ? a.plus(b) : a.minus(b)).simplified();
return {
id: nextId(),
prompt: `${a.toSignedKatex()} ${operation} ${signedOperand(b)}`,
answer: { kind: "fraction", numerator: result.n, denominator: result.d },
hints: [
d1 === d2 ? "The denominators already match." : "Find a common denominator first.",
"Combine the numerators using the integer sign rules (subtracting a negative adds).",
"Simplify your answer.",
],
steps: [
{
explanation: d1 === d2 ? `Keep the denominator ${d1}.` : `Use a common denominator: LCM(${d1}, ${d2}) = ${lcm(d1, d2)}.`,
},
{ explanation: `${operation === "+" ? "Add" : "Subtract"} the numerators, watching the signs.` },
{ explanation: "Simplify to lowest terms.", math: result.toSignedKatex() },
],
};
}
export function generateSignedFractionMultiplyDivide(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["\\times", "\\div"] as const);
const dens = difficulty === 1 ? [2, 3, 4] : difficulty === 2 ? [2, 3, 4, 5] : [3, 4, 5, 6, 7];
const d1 = randomChoice(dens);
const d2 = randomChoice(dens);
const [a, b] = signedFractionPair(d1, d2);
const result = (operation === "\\times" ? a.times(b) : a.dividedBy(b)).simplified();
const sameSign = a.n < 0 === b.n < 0;
return {
id: nextId(),
prompt: `${a.toSignedKatex()} ${operation} ${signedOperand(b)}`,
answer: { kind: "fraction", numerator: result.n, denominator: result.d },
hints: [
operation === "\\div" ? "Keep, change, flip: multiply by the reciprocal." : "Multiply numerators and denominators.",
`The signs are ${sameSign ? "the same, so the answer is positive" : "different, so the answer is negative"}.`,
"Simplify your answer.",
],
steps: [
operation === "\\div"
? { explanation: "Multiply by the reciprocal of the second fraction (keep, change, flip)." }
: { explanation: "Multiply the numerators and multiply the denominators." },
{ explanation: `Same signs give positive, different signs give negative — here the answer is ${result.n < 0 ? "negative" : "positive"}.` },
{ explanation: "Simplify to lowest terms.", math: result.toSignedKatex() },
],
};
}
export function generateFractionBodmas(difficulty: Difficulty): MathProblem {
const dens = difficulty === 1 ? [2, 3, 4] : difficulty === 2 ? [2, 3, 4, 5] : [2, 3, 4, 5, 6];
const mk = () => {
const d = randomChoice(dens);
return new Fraction(randomInt(1, d - 1), d);
};
const f1 = mk(), f2 = mk(), f3 = mk();
const multFirst = Math.random() < 0.5;
let prompt: string;
let result: Fraction;
if (multFirst) {
const prod = f2.times(f3);
// avoid a negative answer in this (positive-fractions) unit
const op = f1.toDecimal() >= prod.toDecimal() && Math.random() < 0.5 ? "-" : "+";
result = (op === "+" ? f1.plus(prod) : f1.minus(prod)).simplified();
prompt = `${f1.toKatex()} ${op} ${f2.toKatex()} \\times ${f3.toKatex()}`;
} else {
const prod = f1.times(f2);
const op = prod.toDecimal() >= f3.toDecimal() && Math.random() < 0.5 ? "-" : "+";
result = (op === "+" ? prod.plus(f3) : prod.minus(f3)).simplified();
prompt = `${f1.toKatex()} \\times ${f2.toKatex()} ${op} ${f3.toKatex()}`;
}
return {
id: nextId(),
prompt,
answer: { kind: "fraction", numerator: result.n, denominator: result.d },
hints: [
"BODMAS: do the multiplication before the addition or subtraction.",
"Multiply the two fractions first, then combine with a common denominator.",
],
steps: [
{ explanation: "Multiply first (that's the B-O-D-M before A-S)." },
{ explanation: "Then add or subtract, using a common denominator." },
{ explanation: "Simplify.", math: result.toKatex() },
],
};
}
/** Combined signed-fraction practice covering +, -, x and / (Unit 5 topic). */
export function generateSignedFraction(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["+", "-", "\\times", "\\div"] as const);
const dens = difficulty === 1 ? [2, 3, 4] : difficulty === 2 ? [2, 3, 4, 5] : [3, 4, 5, 6];
const [a, b] = signedFractionPair(randomChoice(dens), randomChoice(dens));
let result: Fraction;
if (operation === "+") result = a.plus(b);
else if (operation === "-") result = a.minus(b);
else if (operation === "\\times") result = a.times(b);
else result = a.dividedBy(b);
result = result.simplified();
return {
id: nextId(),
prompt: `${a.toSignedKatex()} ${operation} ${signedOperand(b)}`,
answer: { kind: "fraction", numerator: result.n, denominator: result.d },
hints: [
"Apply the integer sign rules as you work.",
"Give your answer in its simplest form.",
],
steps: [
{ explanation: "Work through the operation, watching the signs." },
{ explanation: "Simplify.", math: result.toSignedKatex() },
],
};
}
export function generateWholeFromFraction(difficulty: Difficulty): MathProblem {
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [3, 5, 6, 8] : [7, 8, 9, 12];
const den = randomChoice(denChoices);
const num = randomInt(1, den - 1);
const whole = den * randomChoice(difficulty === 1 ? [2, 3, 4, 5] : [3, 5, 6, 7, 8, 10]);
const part = (num / den) * whole;
return {
id: nextId(),
prompt: `\\frac{${num}}{${den}} \\text{ of a number is } ${part}\\text{. Find the number.}`,
answer: { kind: "integer", value: whole },
hints: [
`If ${num}/${den} of the number is ${part}...`,
`Find 1/${den} first by dividing ${part} by ${num}`,
`Then multiply by ${den} to get the whole`,
],
steps: [
{ explanation: `1/${den} of the number = ${part} ÷ ${num} = ${part / num}` },
{ explanation: `The whole number = ${part / num} × ${den} = ${whole}` },
],
};
}