binary to decimal

This commit is contained in:
2026-05-12 06:26:33 -04:00
parent 7db6fc0bab
commit 32c4035c23
11 changed files with 887 additions and 5 deletions

View File

@@ -0,0 +1,317 @@
"use client";
import { useState, useMemo, useCallback } from "react";
import { Card } from "@/components/ui/card";
const PRESETS = ["101", "1011", "1101", "10101", "11000", "11111", "100000", "110010"];
interface Column {
exponent: number;
placeValue: number;
digit: string;
contrib: number;
isOne: boolean;
}
function powerOfTwo(n: number) {
let v = 1;
for (let i = 0; i < n; i++) v *= 2;
return v;
}
function buildColumnsFor(binaryStr: string): Column[] {
const n = binaryStr.length;
const cols: Column[] = [];
for (let i = 0; i < n; i++) {
const exponent = n - 1 - i;
const digit = binaryStr[i];
const placeValue = powerOfTwo(exponent);
const isOne = digit === "1";
cols.push({
exponent,
placeValue,
digit,
contrib: parseInt(digit, 10) * placeValue,
isOne,
});
}
return cols;
}
function emptyColumns(): Column[] {
const cols: Column[] = [];
for (let exp = 5; exp >= 0; exp--) {
cols.push({
exponent: exp,
placeValue: powerOfTwo(exp),
digit: "_",
contrib: 0,
isOne: false,
});
}
return cols;
}
export function BinaryConverterExplorer() {
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(/[^01]/g, ""));
setError("");
};
const convert = useCallback((raw?: string) => {
const source = (raw ?? input).trim();
if (source.length === 0) {
setError("Type a binary number first (only 0s and 1s).");
setSubmitted(null);
return;
}
if (!/^[01]+$/.test(source)) {
setError("Binary numbers use only 0 and 1. Found another digit.");
setSubmitted(null);
return;
}
if (source.length > 8) {
setError("Keep it to 8 digits or fewer for this lesson.");
return;
}
setError("");
setSubmitted(source);
}, [input]);
const applyPreset = (value: string) => {
setInput(value);
convert(value);
};
const handleClear = () => {
setInput("");
setSubmitted(null);
setError("");
};
const handleRandom = () => {
const len = 3 + Math.floor(Math.random() * 4);
let s = "1";
for (let i = 1; i < len; i++) {
s += Math.random() < 0.5 ? "0" : "1";
}
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.isOne).length > 0
? columns.filter((c) => c.isOne).map((c) => 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 Binary Number
</p>
<div className="flex flex-wrap items-center gap-3">
<label
htmlFor="binaryInput"
className="text-sm font-bold text-unit-6-dark"
>
Binary:
</label>
<input
id="binaryInput"
type="text"
inputMode="numeric"
maxLength={8}
autoComplete="off"
spellCheck={false}
placeholder="e.g. 1011"
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="Binary 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 2)
</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-[64px] border-2 border-unit-6/40 bg-unit-6-light px-4 py-2 text-center text-sm font-bold text-unit-6-dark"
>
2<sup>{c.exponent}</sup>
</td>
))}
</tr>
<tr>
{columns.map((c) => (
<td
key={`v-${c.exponent}`}
className="min-w-[64px] 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-[64px] border-2 border-unit-6/40 px-4 py-2 text-center text-2xl font-extrabold ${
c.digit === "1"
? "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-[64px] border-2 border-unit-6/40 px-4 py-2 text-center text-sm font-bold ${
c.isOne
? "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 binary number above and press Convert.
</p>
) : answerHidden ? (
<p className="font-mono text-lg font-bold text-unit-6">
{submitted}<sub>2</sub> = ? (press Show Answer)
</p>
) : (
<p className="font-mono text-2xl font-extrabold text-correct">
{submitted}<sub>2</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> solve on paper <em>first</em> using the chart in your worksheet. Type the binary number here only to <em>check</em>. The lesson is in the chart, not the click.
</p>
</Card>
</div>
);
}