"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 (
); } const styles: Record = { normal: "border-border bg-surface text-foreground", target: "border-unit-2 bg-unit-2-light text-foreground ring-2 ring-unit-2/30", decider: "border-hint bg-hint-light text-foreground ring-2 ring-hint/30", significant: "border-unit-4 bg-unit-4-light text-foreground ring-2 ring-unit-4/30", dim: "border-border/40 bg-background text-muted/30", }; return (
{char}
); } // ── Explorer component ────────────────────────────────────────────────────── export function RoundingExplorer() { const [input, setInput] = useState("3.4567"); const [target, setTarget] = useState("1dp"); const [error, setError] = useState(""); const [steps, setSteps] = useState(null); const [currentStep, setCurrentStep] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const done = steps ? currentStep >= steps.length - 1 : false; function handleGo() { setError(""); const trimmed = input.trim(); const val = parseFloat(trimmed); if (isNaN(val)) { setError("Enter a valid number."); return; } if (val < 0) { setError("Enter a positive number."); return; } if (trimmed.length > 16) { setError("Number too long."); return; } if (!trimmed.includes(".") && (target === "1dp" || target === "2dp")) { setError("Enter a decimal number for decimal place rounding."); return; } try { const s = buildSteps(trimmed, target); setSteps(s); setCurrentStep(0); setIsPlaying(false); } catch { setError("Could not process. Check your input."); } } const stepForward = useCallback(() => { if (!steps || currentStep >= steps.length - 1) return; setCurrentStep((s) => s + 1); if (currentStep + 1 >= steps.length - 1) setIsPlaying(false); }, [steps, currentStep]); const stepBack = useCallback(() => { if (currentStep <= 0) return; setCurrentStep((s) => s - 1); }, [currentStep]); const togglePlay = useCallback(() => setIsPlaying((p) => !p), []); const reset = useCallback(() => { setSteps(null); setCurrentStep(0); setIsPlaying(false); }, []); const step = steps ? steps[currentStep] : null; const targetLabels: Record = { whole: "Whole Number", "1dp": "1 d.p.", "2dp": "2 d.p.", "1sf": "1 s.f.", "2sf": "2 s.f.", "3sf": "3 s.f.", }; return (
{/* Target tabs */}
{(["whole", "1dp", "2dp", "1sf", "2sf", "3sf"] as RoundTarget[]).map((t) => ( ))}
{/* Input */}

Enter a number

setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleGo()} placeholder="e.g. 3.4567 or 0.00456" maxLength={18} autoComplete="off" className="max-w-[240px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-2" />
{error &&

{error}

}
{/* Display */} {!step ? (

Enter a number above and click Round

) : ( <>

{step.label}

{step.digits.map((d, i) => ( ))}
{/* Legend */}
Target digit Decider digit Significant Not significant
)}
{/* Controls */} 0} /> {/* Result */} {!done || !step?.resultText ? (

Result will appear here when steps are complete

) : ( <>

Rounded Value

{step.resultText}

)}
); }