denary to binary

This commit is contained in:
2026-05-18 21:15:33 -04:00
parent 32c4035c23
commit da82699a4c
3 changed files with 416 additions and 4 deletions

View File

@@ -1,7 +1,12 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(cd \"C:/Users/earl_/source/repos/CabritsMath/cabrits-math\" && npx next build 2>&1 | tail -80)" "Bash(cd \"C:/Users/earl_/source/repos/CabritsMath/cabrits-math\" && npx next build 2>&1 | tail -80)",
"Bash(npx tsc *)",
"Bash(echo \"TSC_EXIT=$?\")",
"Bash(npx eslint *)",
"Bash(echo \"LINT_EXIT=$?\")",
"Bash(npx next *)"
] ]
} }
} }

View File

@@ -1,9 +1,20 @@
"use client"; "use client";
import { useState } from "react";
import { Breadcrumbs } from "@/components/layout/breadcrumbs"; import { Breadcrumbs } from "@/components/layout/breadcrumbs";
import { BinaryConverterExplorer } from "@/components/explorers/binary-converter-explorer"; import { BinaryConverterExplorer } from "@/components/explorers/binary-converter-explorer";
import { DenaryToBinaryExplorer } from "@/components/explorers/denary-to-binary-explorer";
type Mode = "binary-to-denary" | "denary-to-binary";
const MODES: { key: Mode; label: string }[] = [
{ key: "binary-to-denary", label: "Binary → Denary" },
{ key: "denary-to-binary", label: "Denary → Binary" },
];
export default function BinaryPage() { export default function BinaryPage() {
const [mode, setMode] = useState<Mode>("binary-to-denary");
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<Breadcrumbs <Breadcrumbs
@@ -15,11 +26,39 @@ export default function BinaryPage() {
/> />
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Binary &rarr; Decimal Converter</h1> <h1 className="text-3xl font-bold tracking-tight">
<p className="mt-2 text-muted">Same shape, different base &mdash; explore place value in powers of 2.</p> {mode === "binary-to-denary"
? "Binary → Denary Converter"
: "Denary → Binary Converter"}
</h1>
<p className="mt-2 text-muted">
{mode === "binary-to-denary"
? "Same shape, different base — explore place value in powers of 2."
: "Divide by 2. Track the remainder. Read bottom-up."}
</p>
</div> </div>
<div className="flex flex-wrap gap-2">
{MODES.map((m) => (
<button
key={m.key}
onClick={() => setMode(m.key)}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === m.key
? "border-unit-6 bg-unit-6 text-white"
: "border-unit-6/40 text-unit-6 hover:bg-unit-6-light"
}`}
>
{m.label}
</button>
))}
</div>
{mode === "binary-to-denary" ? (
<BinaryConverterExplorer /> <BinaryConverterExplorer />
) : (
<DenaryToBinaryExplorer />
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,368 @@
"use client";
import { useState, useMemo, useCallback } from "react";
import { Card } from "@/components/ui/card";
const COLUMNS = [32, 16, 8, 4, 2, 1];
const PRESETS = [3, 5, 11, 13, 22, 25, 29, 31, 45, 58];
interface DivisionRow {
before: number;
quotient: number;
remainder: number;
}
interface ConversionResult {
rows: DivisionRow[];
bitsLowToHigh: number[];
}
function runDivision(n: number): ConversionResult {
const rows: DivisionRow[] = [];
if (n === 0) {
rows.push({ before: 0, quotient: 0, remainder: 0 });
return { rows, bitsLowToHigh: [0] };
}
let current = n;
while (current > 0) {
const q = Math.floor(current / 2);
const r = current % 2;
rows.push({ before: current, quotient: q, remainder: r });
current = q;
}
return { rows, bitsLowToHigh: rows.map((r) => r.remainder) };
}
function bitsForChart(bitsLowToHigh: number[]): (number | null)[] {
const out: (number | null)[] = Array(COLUMNS.length).fill(null);
for (let i = 0; i < bitsLowToHigh.length && i < COLUMNS.length; i++) {
out[COLUMNS.length - 1 - i] = bitsLowToHigh[i];
}
return out;
}
export function DenaryToBinaryExplorer() {
const [input, setInput] = useState("");
const [submitted, setSubmitted] = useState<number | null>(null);
const [error, setError] = useState("");
const [answerHidden, setAnswerHidden] = useState(false);
const [chartHidden, setChartHidden] = useState(false);
const handleInputChange = (value: string) => {
setInput(value.replace(/[^0-9]/g, "").slice(0, 3));
setError("");
};
const convert = useCallback((raw?: string) => {
const source = (raw ?? input).trim();
if (source === "") {
setError("Type a denary number first (whole number from 1 to 63).");
setSubmitted(null);
return;
}
if (!/^\d+$/.test(source)) {
setError("Use whole digits only (0-9).");
return;
}
const n = parseInt(source, 10);
if (n < 0 || n > 63) {
setError("Pick a whole number between 1 and 63. The chart in this lesson is six columns: 32, 16, 8, 4, 2, 1.");
return;
}
setError("");
setSubmitted(n);
}, [input]);
const applyPreset = (value: number) => {
setInput(String(value));
convert(String(value));
};
const handleClear = () => {
setInput("");
setSubmitted(null);
setError("");
};
const handleRandom = () => {
const n = 1 + Math.floor(Math.random() * 63);
setInput(String(n));
convert(String(n));
};
const result = useMemo(() => {
if (submitted === null) return null;
return runDivision(submitted);
}, [submitted]);
const chartBits = useMemo(() => {
if (!result) return Array(COLUMNS.length).fill(null) as (number | null)[];
return bitsForChart(result.bitsLowToHigh);
}, [result]);
const binaryString = useMemo(() => {
if (!result) return "";
const reversed = result.bitsLowToHigh.slice().reverse().join("");
return reversed.replace(/^0+/, "") || "0";
}, [result]);
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 Denary Number (1 to 63)
</p>
<div className="flex flex-wrap items-center gap-3">
<label htmlFor="denaryInput" className="text-sm font-bold text-unit-6-dark">
Denary:
</label>
<input
id="denaryInput"
type="text"
inputMode="numeric"
maxLength={3}
autoComplete="off"
spellCheck={false}
placeholder="e.g. 13"
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") convert();
}}
className="w-32 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="Denary 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 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>
{/* Division steps */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-6">
Repeated Division by 2
</p>
<div className="rounded-xl border-2 border-hint/40 bg-hint-light/50 p-4">
{!result ? (
<p className="text-center font-mono text-sm text-muted">
Type a denary number above and press Convert to see the division table.
</p>
) : submitted === 0 ? (
<p className="text-center font-mono text-base font-bold text-unit-6">
0 in binary is 0.
</p>
) : (
<div className="overflow-x-auto">
<table className="mx-auto min-w-[380px] border-collapse font-mono text-sm">
<thead>
<tr>
{["Step", "n ÷ 2", "Quotient", "Remainder", ""].map((h, i) => (
<th
key={i}
className={
i === 4
? "px-3 py-2 text-left"
: "border-2 border-unit-6/40 bg-unit-6-light px-4 py-2 text-xs font-bold uppercase tracking-wide text-unit-6-dark"
}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{result.rows.map((row, idx) => (
<tr key={idx}>
<td className="border-2 border-unit-6/40 bg-surface px-4 py-2 text-center font-bold text-unit-6">
{idx + 1}
</td>
<td className="border-2 border-unit-6/40 bg-surface px-4 py-2 text-center text-foreground">
{row.before} ÷ 2
</td>
<td className="border-2 border-unit-6/40 bg-surface px-4 py-2 text-center font-bold text-foreground">
{row.quotient}
</td>
<td className="border-2 border-unit-6/40 bg-correct-light px-4 py-2 text-center text-lg font-extrabold text-correct">
{row.remainder}
</td>
<td className="px-3 py-2 text-left text-sm font-bold italic text-hint">
{idx === 0
? "↓ start"
: idx === result.rows.length - 1
? "↑ read up"
: ""}
</td>
</tr>
))}
<tr>
<td
colSpan={5}
className="border-2 border-unit-6/40 bg-unit-6-light px-4 py-2 text-center text-xs font-bold uppercase tracking-wide text-unit-6-dark"
>
Quotient is 0 stop. Read remainders from bottom to top.
</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
<p className="mt-3 text-sm italic text-hint">
Read the remainders column <strong>from bottom to top</strong>. That string of 0s and 1s is your binary number.
</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((_, idx) => {
const exp = COLUMNS.length - 1 - idx;
return (
<td
key={`p-${exp}`}
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>{exp}</sup>
</td>
);
})}
</tr>
<tr>
{COLUMNS.map((v) => (
<td
key={`v-${v}`}
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"
>
{v}
</td>
))}
</tr>
<tr>
{chartBits.map((b, 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 ${
b === 1
? "bg-correct-light text-correct"
: b === 0
? "bg-surface text-muted"
: "bg-surface text-muted/60"
}`}
>
{b === null ? "-" : b}
</td>
))}
</tr>
<tr>
{chartBits.map((b, i) => (
<td
key={`s-${i}`}
className={`min-w-[64px] border-2 border-unit-6/40 px-4 py-2 text-center text-sm font-bold tracking-wider ${
b === 1
? "bg-correct-light text-correct"
: b === 0
? "bg-[#fafbfc] text-muted"
: "bg-[#fafbfc] text-muted/60"
}`}
>
{b === null ? "OFF" : b === 1 ? "ON" : "OFF"}
</td>
))}
</tr>
</tbody>
</table>
</div>
)}
</Card>
{/* Answer banner */}
<Card
className={`flex min-h-[90px] flex-col items-center justify-center gap-2 text-center ${
submitted !== null && !answerHidden
? "border-correct bg-correct-light"
: "border-unit-6/30 bg-unit-6-light/60"
}`}
>
{submitted === null ? (
<p className="font-mono text-base font-bold text-unit-6">
Type a denary 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} = {binaryString}<sub>2</sub> in binary
</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 division table in your worksheet. Type the denary number here only to <em>check</em>. The lesson is in the division and the bottom-up read, not the click.
</p>
</Card>
</div>
);
}