quarternary added
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
"Bash(echo \"TSC_EXIT=$?\")",
|
"Bash(echo \"TSC_EXIT=$?\")",
|
||||||
"Bash(npx eslint *)",
|
"Bash(npx eslint *)",
|
||||||
"Bash(echo \"LINT_EXIT=$?\")",
|
"Bash(echo \"LINT_EXIT=$?\")",
|
||||||
"Bash(npx next *)"
|
"Bash(npx next *)",
|
||||||
|
"Bash(echo \"BUILD_EXIT=$?\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/lessons/unit-6-number-system/quaternary/page.tsx
Normal file
64
app/lessons/unit-6-number-system/quaternary/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
|
||||||
|
import { QuaternaryToDenaryExplorer } from "@/components/explorers/quaternary-to-denary-explorer";
|
||||||
|
import { DenaryToQuaternaryExplorer } from "@/components/explorers/denary-to-quaternary-explorer";
|
||||||
|
|
||||||
|
type Mode = "quaternary-to-denary" | "denary-to-quaternary";
|
||||||
|
|
||||||
|
const MODES: { key: Mode; label: string }[] = [
|
||||||
|
{ key: "quaternary-to-denary", label: "Quaternary → Denary" },
|
||||||
|
{ key: "denary-to-quaternary", label: "Denary → Quaternary" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function QuaternaryPage() {
|
||||||
|
const [mode, setMode] = useState<Mode>("quaternary-to-denary");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ label: "Lessons", href: "/lessons" },
|
||||||
|
{ label: "Unit 6: Number System", href: "/lessons/unit-6-number-system" },
|
||||||
|
{ label: "Quaternary" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
{mode === "quaternary-to-denary"
|
||||||
|
? "Quaternary → Denary Converter"
|
||||||
|
: "Denary → Quaternary Converter"}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-muted">
|
||||||
|
{mode === "quaternary-to-denary"
|
||||||
|
? "Same shape, different base — explore place value in powers of 4."
|
||||||
|
: "Divide by 4. Track the remainder. Read bottom-up."}
|
||||||
|
</p>
|
||||||
|
</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 === "quaternary-to-denary" ? (
|
||||||
|
<QuaternaryToDenaryExplorer />
|
||||||
|
) : (
|
||||||
|
<DenaryToQuaternaryExplorer />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
368
components/explorers/denary-to-quaternary-explorer.tsx
Normal file
368
components/explorers/denary-to-quaternary-explorer.tsx
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
|
const COLUMNS = [256, 64, 16, 4, 1];
|
||||||
|
const PRESETS = [6, 11, 13, 19, 27, 39, 63, 100, 255];
|
||||||
|
|
||||||
|
interface DivisionRow {
|
||||||
|
before: number;
|
||||||
|
quotient: number;
|
||||||
|
remainder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversionResult {
|
||||||
|
rows: DivisionRow[];
|
||||||
|
digitsLowToHigh: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDivision(n: number): ConversionResult {
|
||||||
|
const rows: DivisionRow[] = [];
|
||||||
|
if (n === 0) {
|
||||||
|
rows.push({ before: 0, quotient: 0, remainder: 0 });
|
||||||
|
return { rows, digitsLowToHigh: [0] };
|
||||||
|
}
|
||||||
|
let current = n;
|
||||||
|
while (current > 0) {
|
||||||
|
const q = Math.floor(current / 4);
|
||||||
|
const r = current % 4;
|
||||||
|
rows.push({ before: current, quotient: q, remainder: r });
|
||||||
|
current = q;
|
||||||
|
}
|
||||||
|
return { rows, digitsLowToHigh: rows.map((r) => r.remainder) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function digitsForChart(digitsLowToHigh: number[]): (number | null)[] {
|
||||||
|
const out: (number | null)[] = Array(COLUMNS.length).fill(null);
|
||||||
|
for (let i = 0; i < digitsLowToHigh.length && i < COLUMNS.length; i++) {
|
||||||
|
out[COLUMNS.length - 1 - i] = digitsLowToHigh[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DenaryToQuaternaryExplorer() {
|
||||||
|
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 255).");
|
||||||
|
setSubmitted(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^\d+$/.test(source)) {
|
||||||
|
setError("Use whole digits only (0-9).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = parseInt(source, 10);
|
||||||
|
if (n < 0 || n > 255) {
|
||||||
|
setError("Pick a whole number between 1 and 255. The chart in this lesson is five columns: 256, 64, 16, 4, 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() * 255);
|
||||||
|
setInput(String(n));
|
||||||
|
convert(String(n));
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = useMemo(() => {
|
||||||
|
if (submitted === null) return null;
|
||||||
|
return runDivision(submitted);
|
||||||
|
}, [submitted]);
|
||||||
|
|
||||||
|
const chartDigits = useMemo(() => {
|
||||||
|
if (!result) return Array(COLUMNS.length).fill(null) as (number | null)[];
|
||||||
|
return digitsForChart(result.digitsLowToHigh);
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
const quaternaryString = useMemo(() => {
|
||||||
|
if (!result) return "";
|
||||||
|
const reversed = result.digitsLowToHigh.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 255)
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<label htmlFor="denaryInputQ" className="text-sm font-bold text-unit-6-dark">
|
||||||
|
Denary:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="denaryInputQ"
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={3}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="e.g. 27"
|
||||||
|
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 4
|
||||||
|
</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 quaternary 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 ÷ 4", "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} ÷ 4
|
||||||
|
</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>. Each remainder is a digit 0, 1, 2, or 3 — that string is your quaternary 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 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((_, idx) => {
|
||||||
|
const exp = COLUMNS.length - 1 - idx;
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={`p-${exp}`}
|
||||||
|
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>{exp}</sup>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{COLUMNS.map((v) => (
|
||||||
|
<td
|
||||||
|
key={`v-${v}`}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{chartDigits.map((d, 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 ${
|
||||||
|
d !== null && d > 0
|
||||||
|
? "bg-correct-light text-correct"
|
||||||
|
: d === 0
|
||||||
|
? "bg-surface text-muted"
|
||||||
|
: "bg-surface text-muted/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{d === null ? "-" : d}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{chartDigits.map((d, i) => (
|
||||||
|
<td
|
||||||
|
key={`s-${i}`}
|
||||||
|
className={`min-w-[72px] border-2 border-unit-6/40 px-4 py-2 text-center text-sm font-bold tracking-wider ${
|
||||||
|
d !== null && d > 0
|
||||||
|
? "bg-correct-light text-correct"
|
||||||
|
: d === 0
|
||||||
|
? "bg-[#fafbfc] text-muted"
|
||||||
|
: "bg-[#fafbfc] text-muted/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{d === null ? "OFF" : d > 0 ? "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>4</sub> (press Show Answer)
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="font-mono text-2xl font-extrabold text-correct">
|
||||||
|
{submitted} = {quaternaryString}<sub>4</sub> in quaternary
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
318
components/explorers/quaternary-to-denary-explorer.tsx
Normal file
318
components/explorers/quaternary-to-denary-explorer.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -215,6 +215,13 @@ export const curriculum: Unit[] = [
|
|||||||
week: 10,
|
week: 10,
|
||||||
description: "Place value in base 2 and converting binary numbers to denary (decimal)",
|
description: "Place value in base 2 and converting binary numbers to denary (decimal)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: "quaternary",
|
||||||
|
title: "Quaternary Numbers",
|
||||||
|
shortTitle: "Quaternary",
|
||||||
|
week: 10,
|
||||||
|
description: "Place value in base 4 and converting between quaternary and denary (decimal)",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user