"use client"; import { useState, useCallback } from "react"; import { Card } from "@/components/ui/card"; import { StepControls } from "./step-controls"; type RoundTarget = "whole" | "1dp" | "2dp" | "1sf" | "2sf" | "3sf"; interface Step { label: string; digits: DigitInfo[]; resultText?: string; } interface DigitInfo { char: string; state: "normal" | "target" | "decider" | "dim" | "significant"; } function getDigits(value: string): string[] { return value.split(""); } // ── Decimal place rounding steps ───────────────────────────────────────────── function buildDPSteps(rawValue: string, target: "whole" | "1dp" | "2dp"): Step[] { const value = parseFloat(rawValue); const digits = getDigits(rawValue); const dotIndex = digits.indexOf("."); const steps: Step[] = []; function makeDigits( targetIdx: number | null, deciderIdx: number | null, ): DigitInfo[] { return digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : i === targetIdx ? "target" : i === deciderIdx ? "decider" : "normal", })); } let targetDigitIdx: number; let roundedValue: number; let targetLabel: string; if (target === "whole") { targetDigitIdx = dotIndex === -1 ? digits.length - 1 : dotIndex - 1; roundedValue = Math.round(value); targetLabel = "ones (whole number)"; } else if (target === "1dp") { targetDigitIdx = dotIndex + 1; roundedValue = Math.round(value * 10) / 10; targetLabel = "1 decimal place (tenths)"; } else { targetDigitIdx = dotIndex + 2; roundedValue = Math.round(value * 100) / 100; targetLabel = "2 decimal places (hundredths)"; } // Compute decider index (skip over decimal point if needed) const nextIdx = targetDigitIdx + 1; const deciderIdx = nextIdx < digits.length && digits[nextIdx] === "." ? nextIdx + 1 : nextIdx; // Step 0: Show the number steps.push({ label: `Rounding ${rawValue} to ${targetLabel}`, digits: makeDigits(null, null), }); // Step 1: Identify target steps.push({ label: `Identify the target digit (${targetLabel})`, digits: makeDigits(targetDigitIdx, null), }); // Step 2: Identify decider if (deciderIdx < digits.length) { steps.push({ label: "Look at the digit to the right (the decider)", digits: makeDigits(targetDigitIdx, deciderIdx), }); const deciderDigit = parseInt(digits[deciderIdx]); const roundsUp = deciderDigit >= 5; steps.push({ label: roundsUp ? `Decider is ${deciderDigit} (≥ 5) → round UP` : `Decider is ${deciderDigit} (< 5) → round DOWN`, digits: makeDigits(targetDigitIdx, deciderIdx), }); } else { steps.push({ label: "No digit to the right — no rounding needed", digits: makeDigits(targetDigitIdx, null), }); } // Step 3: Result const resultStr = target === "whole" ? roundedValue.toString() : target === "1dp" ? roundedValue.toFixed(1) : roundedValue.toFixed(2); const resultDigits = getDigits(resultStr); steps.push({ label: `Result: ${resultStr}`, digits: resultDigits.map((ch) => ({ char: ch, state: "normal" as const, })), resultText: resultStr, }); return steps; } // ── Significant figures rounding steps ─────────────────────────────────────── function buildSFSteps(rawValue: string, sfCount: number): Step[] { const value = parseFloat(rawValue); const digits = getDigits(rawValue); const steps: Step[] = []; // Step 0: Show the number steps.push({ label: `Rounding ${rawValue} to ${sfCount} significant figure${sfCount > 1 ? "s" : ""}`, digits: digits.map((ch) => ({ char: ch, state: "normal" as const })), }); // Step 1: Identify which digits are significant // Rule: significant figures start at the first non-zero digit const sigDigitIndices: number[] = []; let foundFirstNonZero = false; for (let i = 0; i < digits.length; i++) { if (digits[i] === ".") continue; if (!foundFirstNonZero && digits[i] === "0") continue; foundFirstNonZero = true; sigDigitIndices.push(i); } // Build display: highlight all significant digits found const sigSet = new Set(sigDigitIndices); steps.push({ label: `Identify significant figures — start from the first non-zero digit`, digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : sigSet.has(i) ? "significant" : "dim", })), }); // Count and label the sig figs we need const sigLabel = sigDigitIndices.slice(0, sfCount).map((idx, n) => { return `${ordinal(n + 1)} s.f. = ${digits[idx]}`; }); const targetIdx = sigDigitIndices[sfCount - 1]; // the last sig fig we keep if (targetIdx === undefined) { // Not enough significant figures in the number steps.push({ label: `The number only has ${sigDigitIndices.length} significant figure${sigDigitIndices.length !== 1 ? "s" : ""} — already rounded`, digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : sigSet.has(i) ? "significant" : "dim", })), resultText: rawValue, }); return steps; } // Step 2: Show which sig figs we're keeping const keepSet = new Set(sigDigitIndices.slice(0, sfCount)); steps.push({ label: `Keep ${sfCount} sig fig${sfCount > 1 ? "s" : ""}: ${sigLabel.join(", ")}`, digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : keepSet.has(i) ? "target" : sigSet.has(i) ? "normal" : "dim", })), }); // Step 3: Identify the decider const deciderCandidateIdx = sigDigitIndices[sfCount]; // next sig fig after the ones we keep // But the decider could also be after a decimal point let deciderIdx: number | null = null; if (deciderCandidateIdx !== undefined) { deciderIdx = deciderCandidateIdx; } else { // Look for any digit after the target for (let i = targetIdx + 1; i < digits.length; i++) { if (digits[i] !== ".") { deciderIdx = i; break; } } } if (deciderIdx !== null && deciderIdx < digits.length) { steps.push({ label: `Look at the next digit: ${digits[deciderIdx]} (the decider)`, digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : i === deciderIdx ? "decider" : keepSet.has(i) ? "target" : "dim", })), }); // Step 4: Round decision const deciderDigit = parseInt(digits[deciderIdx]); const roundsUp = deciderDigit >= 5; steps.push({ label: roundsUp ? `Decider is ${deciderDigit} (≥ 5) → round UP the last kept digit` : `Decider is ${deciderDigit} (< 5) → round DOWN (keep it the same)`, digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : i === deciderIdx ? "decider" : keepSet.has(i) ? "target" : "dim", })), }); } else { steps.push({ label: "No digit to the right — no rounding needed", digits: digits.map((ch, i) => ({ char: ch, state: ch === "." ? "normal" : keepSet.has(i) ? "target" : "dim", })), }); } // Step 5: Compute and show result let roundedValue: number; if (value === 0) { roundedValue = 0; } else { const magnitude = Math.floor(Math.log10(Math.abs(value))); const factor = Math.pow(10, sfCount - 1 - magnitude); roundedValue = Math.round(value * factor) / factor; } // Format result with correct sig figs (preserve trailing zeros) const resultStr = formatSigFigs(roundedValue, sfCount); const resultDigits = getDigits(resultStr); // Step: explain what happens to remaining digits const dotIdx = rawValue.indexOf("."); const hasIntegerPart = dotIdx === -1 || dotIdx > 0; if (hasIntegerPart && sigDigitIndices.length > sfCount) { // Some trailing digits in the integer part get replaced with zeros steps.push({ label: "Replace remaining digits with zeros (to keep the place value)", digits: resultDigits.map((ch) => ({ char: ch, state: "normal" as const, })), }); } steps.push({ label: `Result: ${resultStr} (${sfCount} significant figure${sfCount > 1 ? "s" : ""})`, digits: resultDigits.map((ch) => ({ char: ch, state: "normal" as const, })), resultText: resultStr, }); return steps; } function formatSigFigs(value: number, sfCount: number): string { if (value === 0) return "0"; const magnitude = Math.floor(Math.log10(Math.abs(value))); if (magnitude >= sfCount - 1) { // Integer result — no decimal point needed return Math.round(value / Math.pow(10, magnitude - sfCount + 1)) * Math.pow(10, magnitude - sfCount + 1) + ""; } // Decimal result — need to show enough places const decimalPlaces = sfCount - 1 - magnitude; return value.toFixed(decimalPlaces); } function ordinal(n: number): string { if (n === 1) return "1st"; if (n === 2) return "2nd"; if (n === 3) return "3rd"; return `${n}th`; } // ── Main step builder ──────────────────────────────────────────────────────── function buildSteps(rawValue: string, target: RoundTarget): Step[] { if (target === "whole" || target === "1dp" || target === "2dp") { return buildDPSteps(rawValue, target); } const sfCount = target === "1sf" ? 1 : target === "2sf" ? 2 : 3; return buildSFSteps(rawValue, sfCount); } // ── Digit box component ───────────────────────────────────────────────────── function DigitBox({ char, state }: DigitInfo) { if (char === ".") { return (
Enter a number
{error}
}Enter a number above and click Round
) : ( <>{step.label}
Result will appear here when steps are complete
) : ( <>Rounded Value
{step.resultText}
> )}