Initial Commit
This commit is contained in:
338
components/explorers/bodmas-explorer.tsx
Normal file
338
components/explorers/bodmas-explorer.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { StepControls } from "./step-controls";
|
||||
import { MathDisplay } from "@/components/math/math-display";
|
||||
import { toKatex, simplify, add, subtract, multiply, divide } from "@/lib/math/fractions";
|
||||
|
||||
interface FractionVal {
|
||||
num: number;
|
||||
den: number;
|
||||
}
|
||||
|
||||
interface Expression {
|
||||
fractions: FractionVal[];
|
||||
operators: string[];
|
||||
}
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
math: string;
|
||||
highlightIndex: number | null; // which operator is being evaluated
|
||||
}
|
||||
|
||||
const OP_PRIORITY: Record<string, number> = {
|
||||
"×": 2,
|
||||
"÷": 2,
|
||||
"+": 1,
|
||||
"−": 1,
|
||||
};
|
||||
|
||||
function performOp(a: FractionVal, b: FractionVal, op: string): FractionVal {
|
||||
let result: [number, number];
|
||||
switch (op) {
|
||||
case "+":
|
||||
result = add(a.num, a.den, b.num, b.den);
|
||||
break;
|
||||
case "−":
|
||||
result = subtract(a.num, a.den, b.num, b.den);
|
||||
break;
|
||||
case "×":
|
||||
result = multiply(a.num, a.den, b.num, b.den);
|
||||
break;
|
||||
case "÷":
|
||||
result = divide(a.num, a.den, b.num, b.den);
|
||||
break;
|
||||
default:
|
||||
result = [0, 1];
|
||||
}
|
||||
return { num: result[0], den: result[1] };
|
||||
}
|
||||
|
||||
function expressionToKatex(fracs: FractionVal[], ops: string[], highlightIdx: number | null): string {
|
||||
const parts: string[] = [];
|
||||
fracs.forEach((f, i) => {
|
||||
parts.push(toKatex(f.num, f.den));
|
||||
if (i < ops.length) {
|
||||
if (i === highlightIdx) {
|
||||
parts.push(`\\;\\boxed{${ops[i]}}\\;`);
|
||||
} else {
|
||||
parts.push(`\\;${ops[i]}\\;`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function buildBodmasSteps(expr: Expression): Step[] {
|
||||
const steps: Step[] = [];
|
||||
let fracs = [...expr.fractions];
|
||||
let ops = [...expr.operators];
|
||||
|
||||
// Step 0: Show the expression
|
||||
steps.push({
|
||||
label: "Start with the expression",
|
||||
math: expressionToKatex(fracs, ops, null),
|
||||
highlightIndex: null,
|
||||
});
|
||||
|
||||
while (ops.length > 0) {
|
||||
// Find highest priority operator (leftmost if tie)
|
||||
let maxPri = 0;
|
||||
let opIdx = 0;
|
||||
ops.forEach((op, i) => {
|
||||
if (OP_PRIORITY[op] > maxPri) {
|
||||
maxPri = OP_PRIORITY[op];
|
||||
opIdx = i;
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight the operation to perform
|
||||
steps.push({
|
||||
label: `BODMAS: do ${ops[opIdx]} first (${OP_PRIORITY[ops[opIdx]] === 2 ? "multiplication/division" : "addition/subtraction"})`,
|
||||
math: expressionToKatex(fracs, ops, opIdx),
|
||||
highlightIndex: opIdx,
|
||||
});
|
||||
|
||||
// Perform the operation
|
||||
const result = performOp(fracs[opIdx], fracs[opIdx + 1], ops[opIdx]);
|
||||
const [sn, sd] = simplify(result.num, result.den);
|
||||
|
||||
const newFracs = [...fracs];
|
||||
newFracs.splice(opIdx, 2, { num: sn, den: sd });
|
||||
const newOps = [...ops];
|
||||
newOps.splice(opIdx, 1);
|
||||
|
||||
steps.push({
|
||||
label: `${fracs[opIdx].num}/${fracs[opIdx].den} ${ops[opIdx]} ${fracs[opIdx + 1].num}/${fracs[opIdx + 1].den} = ${sn}/${sd}`,
|
||||
math: expressionToKatex(newFracs, newOps, null),
|
||||
highlightIndex: null,
|
||||
});
|
||||
|
||||
fracs = newFracs;
|
||||
ops = newOps;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
const PRESETS: { label: string; fracs: FractionVal[]; ops: string[] }[] = [
|
||||
{
|
||||
label: "½ + ¼ × ⅗",
|
||||
fracs: [
|
||||
{ num: 1, den: 2 },
|
||||
{ num: 1, den: 4 },
|
||||
{ num: 3, den: 5 },
|
||||
],
|
||||
ops: ["+", "×"],
|
||||
},
|
||||
{
|
||||
label: "⅔ × ¾ − ½",
|
||||
fracs: [
|
||||
{ num: 2, den: 3 },
|
||||
{ num: 3, den: 4 },
|
||||
{ num: 1, den: 2 },
|
||||
],
|
||||
ops: ["×", "−"],
|
||||
},
|
||||
{
|
||||
label: "⅗ ÷ ¼ + ⅔",
|
||||
fracs: [
|
||||
{ num: 3, den: 5 },
|
||||
{ num: 1, den: 4 },
|
||||
{ num: 2, den: 3 },
|
||||
],
|
||||
ops: ["÷", "+"],
|
||||
},
|
||||
];
|
||||
|
||||
export function BODMASExplorer() {
|
||||
const [fracs, setFracs] = useState<FractionVal[]>([
|
||||
{ num: 1, den: 2 },
|
||||
{ num: 1, den: 4 },
|
||||
{ num: 3, den: 5 },
|
||||
]);
|
||||
const [ops, setOps] = useState<string[]>(["+", "×"]);
|
||||
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 updateFrac(idx: number, field: "num" | "den", value: string) {
|
||||
setFracs((prev) => {
|
||||
const next = [...prev];
|
||||
next[idx] = { ...next[idx], [field]: parseInt(value) || 0 };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function handleGo() {
|
||||
setError("");
|
||||
if (fracs.some((f) => f.den === 0)) {
|
||||
setError("Denominators cannot be zero.");
|
||||
return;
|
||||
}
|
||||
if (fracs.some((f) => Math.abs(f.num) > 99 || Math.abs(f.den) > 99)) {
|
||||
setError("Keep numbers under 100.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const s = buildBodmasSteps({ fractions: fracs, operators: ops });
|
||||
setSteps(s);
|
||||
setCurrentStep(0);
|
||||
setIsPlaying(false);
|
||||
} catch {
|
||||
setError("Could not compute. Check your inputs.");
|
||||
}
|
||||
}
|
||||
|
||||
function loadPreset(idx: number) {
|
||||
const p = PRESETS[idx];
|
||||
setFracs([...p.fracs]);
|
||||
setOps([...p.ops]);
|
||||
reset();
|
||||
}
|
||||
|
||||
function stepForward() {
|
||||
if (!steps || currentStep >= steps.length - 1) return;
|
||||
setCurrentStep((s) => s + 1);
|
||||
if (currentStep + 1 >= steps.length - 1) setIsPlaying(false);
|
||||
}
|
||||
|
||||
function stepBack() {
|
||||
if (currentStep <= 0) return;
|
||||
setCurrentStep((s) => s - 1);
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
setIsPlaying((p) => !p);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setSteps(null);
|
||||
setCurrentStep(0);
|
||||
setIsPlaying(false);
|
||||
}
|
||||
|
||||
const step = steps ? steps[currentStep] : null;
|
||||
|
||||
const opChoices = ["+", "−", "×", "÷"];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Presets */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{PRESETS.map((p, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => loadPreset(i)}
|
||||
className="rounded-full border-2 border-unit-1/40 px-4 py-2 text-sm font-semibold text-unit-1 transition-colors hover:bg-unit-1-light"
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<Card>
|
||||
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-1">
|
||||
Enter a BODMAS expression with fractions
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{fracs.map((f, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<input
|
||||
type="number"
|
||||
value={f.num || ""}
|
||||
onChange={(e) => updateFrac(i, "num", e.target.value)}
|
||||
className="w-12 rounded-lg border-2 border-border bg-surface px-1.5 py-1 text-center text-base font-bold outline-none focus:border-unit-1"
|
||||
aria-label={`Numerator ${i + 1}`}
|
||||
/>
|
||||
<div className="my-0.5 h-0.5 w-10 bg-foreground" />
|
||||
<input
|
||||
type="number"
|
||||
value={f.den || ""}
|
||||
onChange={(e) => updateFrac(i, "den", e.target.value)}
|
||||
className="w-12 rounded-lg border-2 border-border bg-surface px-1.5 py-1 text-center text-base font-bold outline-none focus:border-unit-1"
|
||||
aria-label={`Denominator ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
{i < ops.length && (
|
||||
<select
|
||||
value={ops[i]}
|
||||
onChange={(e) => {
|
||||
const next = [...ops];
|
||||
next[i] = e.target.value;
|
||||
setOps(next);
|
||||
}}
|
||||
className="rounded-lg border-2 border-border bg-surface px-2 py-2 text-center text-lg font-bold text-foreground outline-none focus:border-unit-1"
|
||||
>
|
||||
{opChoices.map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={handleGo}
|
||||
className="rounded-lg bg-unit-1 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-1-dark"
|
||||
>
|
||||
Go →
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
|
||||
</Card>
|
||||
|
||||
{/* Display */}
|
||||
<Card className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-6">
|
||||
{!step ? (
|
||||
<p className="text-muted/50">
|
||||
Enter an expression above and click <strong>Go</strong>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-muted">{step.label}</p>
|
||||
<MathDisplay math={step.math} className="text-2xl" />
|
||||
</>
|
||||
)}
|
||||
</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 || !steps ? (
|
||||
<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">Final Answer</p>
|
||||
<div className="text-3xl font-extrabold max-sm:text-2xl">
|
||||
<MathDisplay math={steps[steps.length - 1].math} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user