diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 30b46fd..9c39bd8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,8 @@ "Bash(echo \"TSC_EXIT=$?\")", "Bash(npx eslint *)", "Bash(echo \"LINT_EXIT=$?\")", - "Bash(npx next *)" + "Bash(npx next *)", + "Bash(echo \"BUILD_EXIT=$?\")" ] } } diff --git a/app/lessons/unit-6-number-system/quaternary/page.tsx b/app/lessons/unit-6-number-system/quaternary/page.tsx new file mode 100644 index 0000000..8cf1347 --- /dev/null +++ b/app/lessons/unit-6-number-system/quaternary/page.tsx @@ -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("quaternary-to-denary"); + + return ( +
+ + +
+

+ {mode === "quaternary-to-denary" + ? "Quaternary → Denary Converter" + : "Denary → Quaternary Converter"} +

+

+ {mode === "quaternary-to-denary" + ? "Same shape, different base — explore place value in powers of 4." + : "Divide by 4. Track the remainder. Read bottom-up."} +

+
+ +
+ {MODES.map((m) => ( + + ))} +
+ + {mode === "quaternary-to-denary" ? ( + + ) : ( + + )} +
+ ); +} diff --git a/components/explorers/denary-to-quaternary-explorer.tsx b/components/explorers/denary-to-quaternary-explorer.tsx new file mode 100644 index 0000000..622e2b2 --- /dev/null +++ b/components/explorers/denary-to-quaternary-explorer.tsx @@ -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(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 ( +
+ {/* Input Card */} + +

+ Type a Denary Number (1 to 255) +

+
+ + 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" + /> + + + +
+ +
+ {PRESETS.map((p) => ( + + ))} +
+ + {error && ( +

+ {error} +

+ )} +
+ + {/* Division steps */} + +

+ Repeated Division by 4 +

+ +
+ {!result ? ( +

+ Type a denary number above and press Convert to see the division table. +

+ ) : submitted === 0 ? ( +

+ 0 in quaternary is 0. +

+ ) : ( +
+ + + + {["Step", "n ÷ 4", "Quotient", "Remainder", ""].map((h, i) => ( + + ))} + + + + {result.rows.map((row, idx) => ( + + + + + + + + ))} + + + + +
+ {h} +
+ {idx + 1} + + {row.before} ÷ 4 + + {row.quotient} + + {row.remainder} + + {idx === 0 + ? "↓ start" + : idx === result.rows.length - 1 + ? "↑ read up" + : ""} +
+ Quotient is 0 — stop. Read remainders from bottom to top. +
+
+ )} +
+ +

+ Read the remainders column from bottom to top. Each remainder is a digit 0, 1, 2, or 3 — that string is your quaternary number. +

+
+ + {/* Place value chart */} + +
+

+ Place Value Chart (powers of 4) +

+ +
+ + {!chartHidden && ( +
+ + + + {COLUMNS.map((_, idx) => { + const exp = COLUMNS.length - 1 - idx; + return ( + + ); + })} + + + {COLUMNS.map((v) => ( + + ))} + + + {chartDigits.map((d, i) => ( + + ))} + + + {chartDigits.map((d, i) => ( + + ))} + + +
+ 4{exp} +
+ {v} +
0 + ? "bg-correct-light text-correct" + : d === 0 + ? "bg-surface text-muted" + : "bg-surface text-muted/60" + }`} + > + {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"} +
+
+ )} +
+ + {/* Answer banner */} + + {submitted === null ? ( +

+ Type a denary number above and press Convert. +

+ ) : answerHidden ? ( +

+ {submitted} = ?4 (press Show Answer) +

+ ) : ( +

+ {submitted} = {quaternaryString}4 in quaternary +

+ )} + +
+ + {/* Footer note */} + +

+ For students: solve on paper first using the division table in your worksheet. Type the denary number here only to check. The lesson is in the division and the bottom-up read, not the click. +

+
+
+ ); +} diff --git a/components/explorers/quaternary-to-denary-explorer.tsx b/components/explorers/quaternary-to-denary-explorer.tsx new file mode 100644 index 0000000..b863ee9 --- /dev/null +++ b/components/explorers/quaternary-to-denary-explorer.tsx @@ -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(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 ( +
+ {/* Input Card */} + +

+ Type a Quaternary Number (digits 0-3) +

+
+ + 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" + /> + + + +
+ +
+ {PRESETS.map((p) => ( + + ))} +
+ + {error && ( +

+ {error} +

+ )} +
+ + {/* Place value chart */} + +
+

+ Place Value Chart (powers of 4) +

+ +
+ + {!chartHidden && ( +
+ + + + {columns.map((c) => ( + + ))} + + + {columns.map((c) => ( + + ))} + + + {columns.map((c, i) => ( + + ))} + + + {columns.map((c, i) => ( + + ))} + + +
+ 4{c.exponent} +
+ {c.placeValue} +
+ {c.digit} +
+ {submitted ? c.contrib : "_"} +
+
+ )} + + {submitted && ( +
+
{fullExpansion}
+
= {onesOnly}
+
+ )} +
+ + {/* Answer banner */} + + {!submitted ? ( +

+ Type a quaternary number above and press Convert. +

+ ) : answerHidden ? ( +

+ {submitted}4 = ? (press Show Answer) +

+ ) : ( +

+ {submitted}4 = {total} in denary +

+ )} + +
+ + {/* Footer note */} + +

+ For students: right-align your base-4 number to the 1s column. Multiply each digit by its place value and add. Solve on paper first, then check here. Digits 4-9 are illegal in base 4 — the input box rejects them. +

+
+
+ ); +} diff --git a/lib/curriculum.ts b/lib/curriculum.ts index 9349735..6df9505 100644 --- a/lib/curriculum.ts +++ b/lib/curriculum.ts @@ -215,6 +215,13 @@ export const curriculum: Unit[] = [ week: 10, 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)", + }, ], }, ];