Files
cabrits-math/components/explorers/decimal-arithmetic-explorer.tsx
2026-03-01 18:50:29 -04:00

404 lines
12 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.
"use client";
import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls";
type DecOp = "add" | "subtract" | "multiply" | "divide";
interface Step {
label: string;
columns: ColumnDisplay[];
carry?: string;
resultRow?: string;
}
interface ColumnDisplay {
rows: string[];
highlight?: number; // which row is highlighted
separator?: boolean; // line above this row
}
function padAndAlign(a: string, b: string): { aStr: string; bStr: string; dotPos: number } {
const aParts = a.split(".");
const bParts = b.split(".");
const aInt = aParts[0] || "0";
const bInt = bParts[0] || "0";
const aDec = aParts[1] || "";
const bDec = bParts[1] || "";
const maxInt = Math.max(aInt.length, bInt.length);
const maxDec = Math.max(aDec.length, bDec.length);
const aAligned = aInt.padStart(maxInt, " ") + (maxDec > 0 ? "." + aDec.padEnd(maxDec, "0") : "");
const bAligned = bInt.padStart(maxInt, " ") + (maxDec > 0 ? "." + bDec.padEnd(maxDec, "0") : "");
return { aStr: aAligned, bStr: bAligned, dotPos: maxInt };
}
function buildAddSubSteps(a: number, b: number, op: DecOp): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const { aStr: aAligned, bStr: bAligned } = padAndAlign(aStr, bStr);
const opSymbol = op === "add" ? "+" : "";
const result = op === "add" ? a + b : a - b;
const resultStr = parseFloat(result.toFixed(10)).toString();
// Step 0: Show the problem
steps.push({
label: `${aStr} ${opSymbol} ${bStr}`,
columns: [],
});
// Step 1: Align decimal points
steps.push({
label: "Align the decimal points",
columns: [
{ rows: [aAligned, `${opSymbol} ${bAligned}`] },
],
});
// Step 2: Add trailing zeros
steps.push({
label: "Fill in zeros as placeholders",
columns: [
{ rows: [aAligned, `${opSymbol} ${bAligned}`], separator: true },
],
});
// Step 3: Compute
const { aStr: aFinal, bStr: bFinal } = padAndAlign(aStr, bStr);
const resultAligned = padAndAlign(resultStr, aStr).aStr;
steps.push({
label: `Compute column by column`,
columns: [
{ rows: [aFinal, `${opSymbol} ${bFinal}`, resultAligned], separator: true, highlight: 2 },
],
});
// Step 4: Result
steps.push({
label: `${aStr} ${opSymbol} ${bStr} = ${resultStr}`,
columns: [
{ rows: [aFinal, `${opSymbol} ${bFinal}`, resultAligned], separator: true, highlight: 2 },
],
resultRow: resultStr,
});
return steps;
}
function buildMultiplySteps(a: number, b: number): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const aDec = (aStr.split(".")[1] || "").length;
const bDec = (bStr.split(".")[1] || "").length;
const totalDec = aDec + bDec;
// Remove decimals for integer multiplication
const aInt = Math.round(a * Math.pow(10, aDec));
const bInt = Math.round(b * Math.pow(10, bDec));
const result = a * b;
const resultStr = parseFloat(result.toFixed(10)).toString();
steps.push({
label: `${aStr} × ${bStr}`,
columns: [],
});
if (totalDec > 0) {
steps.push({
label: `Count decimal places: ${aDec} + ${bDec} = ${totalDec}`,
columns: [
{ rows: [`${aStr}${aDec} d.p.`, `${bStr}${bDec} d.p.`, `Total: ${totalDec} d.p.`] },
],
});
steps.push({
label: `Multiply as whole numbers: ${aInt} × ${bInt}`,
columns: [
{ rows: [`${aInt}`, `× ${bInt}`, `${aInt * bInt}`], separator: true, highlight: 2 },
],
});
steps.push({
label: `Place decimal point ${totalDec} place${totalDec !== 1 ? "s" : ""} from the right`,
columns: [
{ rows: [`${aInt * bInt}`, `${resultStr}`], highlight: 1 },
],
resultRow: resultStr,
});
} else {
const intResult = a * b;
steps.push({
label: `Multiply: ${aStr} × ${bStr} = ${intResult}`,
columns: [
{ rows: [`${aStr}`, `× ${bStr}`, `${intResult}`], separator: true, highlight: 2 },
],
resultRow: intResult.toString(),
});
}
return steps;
}
function buildDivideSteps(a: number, b: number): Step[] {
const steps: Step[] = [];
const aStr = a.toString();
const bStr = b.toString();
const result = a / b;
const resultStr = parseFloat(result.toFixed(10)).toString();
steps.push({
label: `${aStr} ÷ ${bStr}`,
columns: [],
});
// Make divisor a whole number
const bDec = (bStr.split(".")[1] || "").length;
if (bDec > 0) {
const factor = Math.pow(10, bDec);
const newA = parseFloat((a * factor).toFixed(10));
const newB = parseFloat((b * factor).toFixed(10));
steps.push({
label: `Make divisor whole: multiply both by ${factor}`,
columns: [
{ rows: [`${aStr} × ${factor} = ${newA}`, `${bStr} × ${factor} = ${newB}`] },
],
});
steps.push({
label: `Now divide: ${newA} ÷ ${newB}`,
columns: [
{ rows: [`${newA} ÷ ${newB}`, `= ${resultStr}`], highlight: 1 },
],
});
} else {
steps.push({
label: `Divide: ${aStr} ÷ ${bStr}`,
columns: [
{ rows: [`${aStr} ÷ ${bStr}`, `= ${resultStr}`], highlight: 1 },
],
});
}
steps.push({
label: `${aStr} ÷ ${bStr} = ${resultStr}`,
columns: [
{ rows: [`${aStr} ÷ ${bStr} = ${resultStr}`] },
],
resultRow: resultStr,
});
return steps;
}
function ColumnArithmetic({
columns,
}: {
columns: ColumnDisplay[];
}) {
if (columns.length === 0) return null;
return (
<div className="flex flex-col items-center gap-0.5 font-mono text-xl">
{columns.map((col, ci) =>
col.rows.map((row, ri) => (
<div key={`${ci}-${ri}`}>
{col.separator && ri === col.rows.length - 1 && (
<div className="mb-1 h-0.5 bg-foreground" />
)}
<div
className={`rounded-lg px-6 py-1.5 text-right tracking-wider transition-all duration-300 ${
ri === col.highlight
? "bg-unit-3-light font-bold text-foreground"
: "text-foreground"
}`}
>
{row}
</div>
</div>
)),
)}
</div>
);
}
export function DecimalArithmeticExplorer() {
const [op, setOp] = useState<DecOp>("add");
const [aInput, setAInput] = useState("12.45");
const [bInput, setBInput] = useState("3.7");
const [error, setError] = useState("");
const [steps, setSteps] = useState<Step[] | null>(null);
const [currentStep, setCurrentStep] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const done = steps ? currentStep >= steps.length - 1 : false;
function handleGo() {
setError("");
const a = parseFloat(aInput);
const b = parseFloat(bInput);
if (isNaN(a) || isNaN(b)) {
setError("Enter valid decimal numbers.");
return;
}
if (op === "divide" && b === 0) {
setError("Cannot divide by zero.");
return;
}
try {
let s: Step[];
if (op === "add" || op === "subtract") {
s = buildAddSubSteps(a, b, op);
} else if (op === "multiply") {
s = buildMultiplySteps(a, b);
} else {
s = buildDivideSteps(a, b);
}
setSteps(s);
setCurrentStep(0);
setIsPlaying(false);
} catch {
setError("Could not compute. Check your inputs.");
}
}
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 opLabels: Record<DecOp, string> = {
add: "Add (+)",
subtract: "Subtract ()",
multiply: "Multiply (×)",
divide: "Divide (÷)",
};
return (
<div className="space-y-4">
{/* Operation tabs */}
<div className="flex flex-wrap gap-2">
{(["add", "subtract", "multiply", "divide"] as DecOp[]).map((o) => (
<button
key={o}
onClick={() => {
setOp(o);
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
op === o
? "border-unit-3 bg-unit-3 text-white"
: "border-unit-3/40 text-unit-3 hover:bg-unit-3-light"
}`}
>
{opLabels[o]}
</button>
))}
</div>
{/* Input */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-3">
Enter two decimal numbers
</p>
<div className="flex flex-wrap items-center gap-2.5">
<input
type="text"
value={aInput}
onChange={(e) => setAInput(e.target.value)}
placeholder="e.g. 12.45"
autoComplete="off"
className="max-w-[140px] 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-3"
aria-label="First number"
/>
<span className="text-xl font-bold text-muted">
{{ add: "+", subtract: "", multiply: "×", divide: "÷" }[op]}
</span>
<input
type="text"
value={bInput}
onChange={(e) => setBInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleGo()}
placeholder="e.g. 3.7"
autoComplete="off"
className="max-w-[140px] 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-3"
aria-label="Second number"
/>
<button
onClick={handleGo}
className="rounded-lg bg-unit-3 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-3-dark"
>
Go
</button>
</div>
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
</Card>
{/* Display */}
<Card className="flex min-h-[220px] flex-col items-center justify-center gap-4 p-6">
{!step ? (
<p className="text-muted/50">
Enter numbers above and click <strong>Go</strong>
</p>
) : (
<>
<p className="text-sm font-medium text-muted">{step.label}</p>
<ColumnArithmetic columns={step.columns} />
</>
)}
</Card>
{/* Controls */}
<Card className="p-4">
<StepControls
currentStep={currentStep + 1}
totalSteps={steps?.length ?? 0}
isPlaying={isPlaying}
onStepForward={stepForward}
onStepBack={stepBack}
onTogglePlay={togglePlay}
onReset={reset}
canStepForward={!!steps && !done}
canStepBack={!!steps && currentStep > 0}
/>
</Card>
{/* Result */}
<Card className="flex min-h-[80px] flex-col items-center justify-center gap-1.5 text-center">
{!done || !step?.resultRow ? (
<p className="text-sm text-muted/40">Result will appear here when steps are complete</p>
) : (
<>
<p className="text-xs uppercase tracking-wider text-muted">Answer</p>
<p className="text-3xl font-extrabold text-foreground max-sm:text-2xl">
{step.resultRow}
</p>
</>
)}
</Card>
</div>
);
}