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; }