Files
cabrits-math/lib/problems/generators/decimal-problems.ts
2026-03-01 18:50:29 -04:00

288 lines
9.2 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, shuffle } from "@/lib/utils";
import { roundToDecimalPlaces, roundToSignificantFigures, toStandardForm, standardFormToKatex } from "@/lib/math/decimals";
let counter = 0;
function nextId() {
return `dp-${++counter}-${Date.now()}`;
}
export function generateDecimalCompareOrder(difficulty: Difficulty): MathProblem {
const count = difficulty === 1 ? 3 : difficulty === 2 ? 4 : 5;
const values: number[] = [];
for (let i = 0; i < count; i++) {
const intPart = randomInt(0, difficulty === 1 ? 2 : 10);
const decPlaces = randomInt(1, difficulty === 1 ? 2 : 3);
const decPart = randomInt(1, Math.pow(10, decPlaces) - 1);
const val = intPart + decPart / Math.pow(10, decPlaces);
values.push(Math.round(val * 1000) / 1000);
}
const sorted = [...values].sort((a, b) => a - b);
const shuffled = shuffle(values);
return {
id: nextId(),
prompt: `\\text{Order from smallest to largest: } ${shuffled.join(", ")}`,
answer: { kind: "decimal", value: 0 },
hints: [
"Compare the whole number part first",
"If the whole parts are equal, compare tenths, then hundredths",
"Writing them with the same number of decimal places can help",
],
steps: sorted.map((v, i) => ({
explanation: `Position ${i + 1}: ${v}`,
})),
};
}
export function generateDecimalRounding(difficulty: Difficulty): MathProblem {
const value = randomInt(100, 99999) / (difficulty === 1 ? 100 : 1000);
const roundType = randomChoice(
difficulty === 1
? ["whole", "1dp"] as const
: difficulty === 2
? ["1dp", "2dp", "whole"] as const
: ["1sf", "2sf", "3sf"] as const,
);
let answer: number;
let instruction: string;
switch (roundType) {
case "whole":
answer = Math.round(value);
instruction = "the nearest whole number";
break;
case "1dp":
answer = roundToDecimalPlaces(value, 1);
instruction = "1 decimal place";
break;
case "2dp":
answer = roundToDecimalPlaces(value, 2);
instruction = "2 decimal places";
break;
case "1sf":
answer = roundToSignificantFigures(value, 1);
instruction = "1 significant figure";
break;
case "2sf":
answer = roundToSignificantFigures(value, 2);
instruction = "2 significant figures";
break;
case "3sf":
answer = roundToSignificantFigures(value, 3);
instruction = "3 significant figures";
break;
}
return {
id: nextId(),
prompt: `\\text{Round } ${value} \\text{ to ${instruction}}`,
answer: { kind: "decimal", value: answer },
hints: [
"Look at the digit to the right of where you're rounding",
"If it's 5 or more, round up. If it's less than 5, round down",
],
steps: [
{ explanation: `Rounding ${value} to ${instruction}` },
{ explanation: `Answer: ${answer}` },
],
};
}
export function generateStandardForm(difficulty: Difficulty): MathProblem {
let value: number;
if (difficulty === 1) {
value = randomChoice([7438, 1578, 62, 93000, 5200, 340]);
} else if (difficulty === 2) {
value = randomChoice([0.086, 0.748, 0.0055, 0.34, 0.007]);
} else {
value = randomChoice([12436.3, 0.00092, 504000, 0.0000067, 83100]);
}
const sf = toStandardForm(value);
return {
id: nextId(),
prompt: `\\text{Express } ${value} \\text{ in standard form}`,
answer: { kind: "standardForm", coefficient: sf.coefficient, exponent: sf.exponent },
hints: [
"Move the decimal point so there's one non-zero digit before it",
"Count how many places you moved the decimal",
value >= 1
? "Number is ≥ 1, so the power is positive"
: "Number is < 1, so the power is negative",
],
steps: [
{ explanation: `Move the decimal point to get a number between 1 and 10` },
{ explanation: `Coefficient: ${sf.coefficient}` },
{ explanation: `The decimal moved ${Math.abs(sf.exponent)} places, power = ${sf.exponent}` },
{ explanation: "Answer", math: standardFormToKatex(sf.coefficient, sf.exponent) },
],
};
}
export function generateDecimalAddSubtract(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["add", "subtract"] as const);
const op = operation === "add" ? "+" : "-";
const places = difficulty === 1 ? 1 : difficulty === 2 ? 2 : 3;
const a = randomInt(100, 9999) / Math.pow(10, places);
const b = randomInt(100, 9999) / Math.pow(10, places);
const [first, second] = operation === "subtract" ? [Math.max(a, b), Math.min(a, b)] : [a, b];
const answer = roundToDecimalPlaces(
operation === "add" ? first + second : first - second,
places,
);
return {
id: nextId(),
prompt: `${first} ${op} ${second}`,
answer: { kind: "decimal", value: answer },
hints: [
"Line up the decimal points",
"Add zeros to fill empty places",
operation === "add" ? "Add column by column from right to left" : "Subtract column by column, borrow if needed",
],
steps: [
{ explanation: `Line up: ${first} ${op} ${second}` },
{ explanation: `= ${answer}` },
],
};
}
export function generateDecimalMultiplyDivide(difficulty: Difficulty): MathProblem {
const operation = randomChoice(["multiply", "divide"] as const);
if (difficulty === 1) {
// By powers of 10
const power = randomChoice([10, 100, 1000]);
const base = randomInt(1, 999) / 100;
if (operation === "multiply") {
const answer = roundToDecimalPlaces(base * power, 10);
return {
id: nextId(),
prompt: `${base} \\times ${power}`,
answer: { kind: "decimal", value: answer },
hints: [
`When multiplying by ${power}, move decimal ${Math.log10(power)} place(s) right`,
],
steps: [
{ explanation: `Move decimal point ${Math.log10(power)} place(s) to the right` },
{ explanation: `= ${answer}` },
],
};
} else {
const answer = roundToDecimalPlaces(base / power, 10);
return {
id: nextId(),
prompt: `${base} \\div ${power}`,
answer: { kind: "decimal", value: answer },
hints: [
`When dividing by ${power}, move decimal ${Math.log10(power)} place(s) left`,
],
steps: [
{ explanation: `Move decimal point ${Math.log10(power)} place(s) to the left` },
{ explanation: `= ${answer}` },
],
};
}
}
// By whole numbers or decimals
const a = randomInt(10, 999) / 10;
const b = difficulty === 2 ? randomInt(2, 12) : randomInt(10, 99) / 10;
if (operation === "multiply") {
const answer = roundToDecimalPlaces(a * b, 4);
return {
id: nextId(),
prompt: `${a} \\times ${b}`,
answer: { kind: "decimal", value: answer },
hints: [
"Multiply as if there are no decimal points",
"Count total decimal places in both numbers",
"Place the decimal point in your answer",
],
steps: [
{ explanation: `${a} × ${b}` },
{ explanation: `= ${answer}` },
],
};
} else {
const answer = roundToDecimalPlaces(a / b, 4);
return {
id: nextId(),
prompt: `${a} \\div ${b}`,
answer: { kind: "decimal", value: answer },
hints: [
"Place decimal point in answer directly above the one in the dividend",
"Divide as normal",
],
steps: [
{ explanation: `${a} ÷ ${b}` },
{ explanation: `= ${answer}` },
],
};
}
}
export function generateDecimalConversion(difficulty: Difficulty): MathProblem {
const direction = randomChoice(["toFraction", "toDecimal"] as const);
if (direction === "toFraction") {
const decimal = randomChoice(
difficulty === 1
? [0.5, 0.25, 0.75, 0.1, 0.2, 0.4]
: [0.125, 0.375, 0.625, 0.875, 0.15, 0.35, 0.45],
);
const den = decimal === 0.5 ? 2 : decimal === 0.25 || decimal === 0.75 ? 4 : [0.125, 0.375, 0.625, 0.875].includes(decimal) ? 8 : 100;
const num = Math.round(decimal * den);
const g = gcdSimple(num, den);
return {
id: nextId(),
prompt: `\\text{Convert } ${decimal} \\text{ to a fraction}`,
answer: { kind: "fraction", numerator: num / g, denominator: den / g },
hints: [
`Write as ${num}/${den}`,
"Simplify by dividing numerator and denominator by their GCD",
],
steps: [
{ explanation: `${decimal} = ${num}/${den}` },
{ explanation: `Simplified: ${num / g}/${den / g}` },
],
};
} else {
const den = randomChoice(difficulty === 1 ? [2, 4, 5, 10] : [3, 8, 20, 25, 40, 50]);
const num = randomInt(1, den - 1);
const answer = roundToDecimalPlaces(num / den, 6);
return {
id: nextId(),
prompt: `\\text{Convert } \\frac{${num}}{${den}} \\text{ to a decimal}`,
answer: { kind: "decimal", value: answer },
hints: [
`Divide ${num} by ${den}`,
"Or find an equivalent fraction with denominator 10, 100, etc.",
],
steps: [
{ explanation: `${num} ÷ ${den} = ${answer}` },
],
};
}
}
function gcdSimple(a: number, b: number): number {
a = Math.abs(a);
b = Math.abs(b);
while (b) {
[a, b] = [b, a % b];
}
return a;
}