Files
cabrits-math/components/explorers/quaternary-to-denary-explorer.tsx
2026-06-01 17:15:03 -04:00

319 lines
10 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, useMemo, useCallback } from "react";
import { Card } from "@/components/ui/card";
const PRESETS = ["3", "12", "23", "33", "123", "213", "321", "1230", "3333"];
interface Column {
exponent: number;
placeValue: number;
digit: string;
contrib: number;
isOn: boolean;
}
function powerOfFour(n: number) {
let v = 1;
for (let i = 0; i < n; i++) v *= 4;
return v;
}
function buildColumnsFor(quaternaryStr: string): Column[] {
const n = quaternaryStr.length;
const cols: Column[] = [];
for (let i = 0; i < n; i++) {
const exponent = n - 1 - i;
const digit = quaternaryStr[i];
const placeValue = powerOfFour(exponent);
const value = parseInt(digit, 10);
cols.push({
exponent,
placeValue,
digit,
contrib: value * placeValue,
isOn: value > 0,
});
}
return cols;
}
function emptyColumns(): Column[] {
const cols: Column[] = [];
for (let exp = 4; exp >= 0; exp--) {
cols.push({
exponent: exp,
placeValue: powerOfFour(exp),
digit: "_",
contrib: 0,
isOn: false,
});
}
return cols;
}
export function QuaternaryToDenaryExplorer() {
const [input, setInput] = useState("");
const [submitted, setSubmitted] = useState<string | null>(null);
const [error, setError] = useState("");
const [answerHidden, setAnswerHidden] = useState(false);
const [chartHidden, setChartHidden] = useState(false);
const handleInputChange = (value: string) => {
setInput(value.replace(/[^0-3]/g, ""));
setError("");
};
const convert = useCallback((raw?: string) => {
const source = (raw ?? input).trim();
if (source.length === 0) {
setError("Type a quaternary number first (digits 0-3 only).");
setSubmitted(null);
return;
}
if (!/^[0-3]+$/.test(source)) {
setError("Quaternary numbers use only digits 0, 1, 2 and 3.");
setSubmitted(null);
return;
}
if (source.length > 5) {
setError("Keep it to 5 digits or fewer (largest is 33333₄ = 1023).");
return;
}
setError("");
setSubmitted(source);
}, [input]);
const applyPreset = (value: string) => {
setInput(value);
convert(value);
};
const handleClear = () => {
setInput("");
setSubmitted(null);
setError("");
};
const handleRandom = () => {
const len = 2 + Math.floor(Math.random() * 3);
const first = 1 + Math.floor(Math.random() * 3);
let s = String(first);
for (let i = 1; i < len; i++) {
s += String(Math.floor(Math.random() * 4));
}
setInput(s);
convert(s);
};
const columns = useMemo(() => {
if (!submitted) return emptyColumns();
return buildColumnsFor(submitted);
}, [submitted]);
const total = useMemo(() => {
if (!submitted) return 0;
return columns.reduce((acc, c) => acc + c.contrib, 0);
}, [columns, submitted]);
const fullExpansion = submitted
? columns.map((c) => `${c.digit}×${c.placeValue}`).join(" + ")
: "";
const onesOnly = submitted
? columns.filter((c) => c.isOn).length > 0
? columns.filter((c) => c.isOn).map((c) => `${c.digit}×${c.placeValue}`).join(" + ") + " = " + total
: "0"
: "";
return (
<div className="space-y-4">
{/* Input Card */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-6">
Type a Quaternary Number (digits 0-3)
</p>
<div className="flex flex-wrap items-center gap-3">
<label
htmlFor="quaternaryInput"
className="text-sm font-bold text-unit-6-dark"
>
Base 4:
</label>
<input
id="quaternaryInput"
type="text"
inputMode="numeric"
maxLength={5}
autoComplete="off"
spellCheck={false}
placeholder="e.g. 123"
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") convert();
}}
className="w-44 rounded-lg border-2 border-unit-6 bg-surface px-3 py-2 text-center font-mono text-2xl font-bold tracking-[0.3em] text-unit-6 outline-none focus:border-unit-6-dark"
aria-label="Quaternary number input"
/>
<button
onClick={() => convert()}
className="rounded-lg bg-unit-6 px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-6-dark"
>
Convert
</button>
<button
onClick={handleClear}
className="rounded-lg border-2 border-border bg-surface px-4 py-2 text-sm font-bold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
Clear
</button>
<button
onClick={handleRandom}
className="rounded-lg border-2 border-border bg-surface px-4 py-2 text-sm font-bold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
Random
</button>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{PRESETS.map((p) => (
<button
key={p}
onClick={() => applyPreset(p)}
className="rounded-lg border-2 border-unit-6/30 bg-unit-6-light px-3 py-1 font-mono text-sm font-bold tracking-widest text-unit-6-dark transition-colors hover:border-unit-6 hover:bg-unit-6 hover:text-white"
>
{p}
</button>
))}
</div>
{error && (
<p className="mt-3 rounded-lg border-2 border-incorrect bg-incorrect-light px-3 py-2 text-center text-sm font-bold text-incorrect">
{error}
</p>
)}
</Card>
{/* Place value chart */}
<Card>
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<p className="text-xs font-bold uppercase tracking-wider text-unit-6">
Place Value Chart (powers of 4)
</p>
<button
onClick={() => setChartHidden((v) => !v)}
className="rounded-md border border-border bg-surface px-2.5 py-1 text-xs font-semibold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
{chartHidden ? "Show Chart" : "Hide Chart"}
</button>
</div>
{!chartHidden && (
<div className="overflow-x-auto rounded-xl border-2 border-unit-6/30 bg-unit-6-light/40 p-3">
<table className="mx-auto border-collapse font-mono">
<tbody>
<tr>
{columns.map((c) => (
<td
key={`p-${c.exponent}`}
className="min-w-[72px] border-2 border-unit-6/40 bg-unit-6-light px-4 py-2 text-center text-sm font-bold text-unit-6-dark"
>
4<sup>{c.exponent}</sup>
</td>
))}
</tr>
<tr>
{columns.map((c) => (
<td
key={`v-${c.exponent}`}
className="min-w-[72px] border-2 border-unit-6/40 bg-surface px-4 py-2 text-center text-base font-bold text-unit-6"
>
{c.placeValue}
</td>
))}
</tr>
<tr>
{columns.map((c, i) => (
<td
key={`d-${i}`}
className={`min-w-[72px] border-2 border-unit-6/40 px-4 py-2 text-center text-2xl font-extrabold ${
c.isOn
? "bg-correct-light text-correct"
: c.digit === "_"
? "bg-surface text-muted/60"
: "bg-surface text-muted"
}`}
>
{c.digit}
</td>
))}
</tr>
<tr>
{columns.map((c, i) => (
<td
key={`c-${i}`}
className={`min-w-[72px] border-2 border-unit-6/40 px-4 py-2 text-center text-sm font-bold ${
c.isOn
? "bg-correct-light text-correct"
: c.digit === "_"
? "bg-[#fafbfc] text-muted/60"
: "bg-[#fafbfc] text-muted"
}`}
>
{submitted ? c.contrib : "_"}
</td>
))}
</tr>
</tbody>
</table>
</div>
)}
{submitted && (
<div className="mt-4 text-center font-mono text-base font-bold text-unit-6">
<div>{fullExpansion}</div>
<div>= {onesOnly}</div>
</div>
)}
</Card>
{/* Answer banner */}
<Card
className={`flex min-h-[90px] flex-col items-center justify-center gap-2 text-center ${
submitted && !answerHidden
? "border-correct bg-correct-light"
: "border-unit-6/30 bg-unit-6-light/60"
}`}
>
{!submitted ? (
<p className="font-mono text-base font-bold text-unit-6">
Type a quaternary number above and press Convert.
</p>
) : answerHidden ? (
<p className="font-mono text-lg font-bold text-unit-6">
{submitted}<sub>4</sub> = ? (press Show Answer)
</p>
) : (
<p className="font-mono text-2xl font-extrabold text-correct">
{submitted}<sub>4</sub> = {total} in denary
</p>
)}
<button
onClick={() => setAnswerHidden((v) => !v)}
className="mt-1 rounded-md border border-border bg-surface px-3 py-1 text-xs font-semibold text-muted transition-colors hover:border-unit-6/50 hover:text-unit-6"
>
{answerHidden ? "Show Answer" : "Hide Answer"}
</button>
</Card>
{/* Footer note */}
<Card className="border-l-4 border-l-unit-6 bg-unit-6-light/40">
<p className="text-sm text-unit-6-dark">
<strong>For students:</strong> right-align your base-4 number to the 1s column. Multiply each digit by its place value and add. Solve on paper <em>first</em>, then check here. Digits 4-9 are illegal in base 4 the input box rejects them.
</p>
</Card>
</div>
);
}