Initial Commit
This commit is contained in:
403
components/explorers/decimal-arithmetic-explorer.tsx
Normal file
403
components/explorers/decimal-arithmetic-explorer.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user