From e3b0555b871f1cb9c2e8c139773edc47c46e036c Mon Sep 17 00:00:00 2001 From: Jagadeesh729 Date: Mon, 22 Jun 2026 17:00:41 +0530 Subject: [PATCH] feat: add Number Base Converter tool --- src/constants/tools.ts | 6 + .../NumberBaseConverter.tsx | 298 ++++++++++++++++++ src/pages/tools/number-base-converter.astro | 20 ++ 3 files changed, 324 insertions(+) create mode 100644 src/features/number-base-converter/NumberBaseConverter.tsx create mode 100644 src/pages/tools/number-base-converter.astro diff --git a/src/constants/tools.ts b/src/constants/tools.ts index 97d8650..c41aa84 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -96,6 +96,12 @@ export const tools: Tool[] = [ href: "/tools/text-to-binary", icon: ``, }, + { + name: "Number Base Converter", + description: "Convert numbers between Decimal, Binary, Hexadecimal, and Octal in real time.", + href: "/tools/number-base-converter", + icon: ``, + }, { name: "Lorem Ipsum Generator", description: "Generate customizable Lorem Ipsum placeholder text by words, sentences, or paragraphs.", diff --git a/src/features/number-base-converter/NumberBaseConverter.tsx b/src/features/number-base-converter/NumberBaseConverter.tsx new file mode 100644 index 0000000..23076db --- /dev/null +++ b/src/features/number-base-converter/NumberBaseConverter.tsx @@ -0,0 +1,298 @@ +import { useState } from "react"; +import CopyButton from "../../ui/CopyButton"; +import ToolActions from "../../components/tool/ToolActions"; +import Button from "../../ui/Button"; + +// ── Helper functions for parsing & cleaning ────────────────────────────────── + +function getCleanValue(val: string, base: number): { cleaned: string; isNegative: boolean } { + // Strip spaces, underscores, commas + let trimmed = val.trim().replace(/_/g, "").replace(/,/g, "").replace(/\s+/g, ""); + const isNegative = trimmed.startsWith("-"); + if (isNegative) { + trimmed = trimmed.slice(1); + } + if (base === 16) { + if (trimmed.toLowerCase().startsWith("0x")) { + trimmed = trimmed.slice(2); + } + } else if (base === 2) { + if (trimmed.toLowerCase().startsWith("0b")) { + trimmed = trimmed.slice(2); + } + } else if (base === 8) { + if (trimmed.toLowerCase().startsWith("0o")) { + trimmed = trimmed.slice(2); + } + } + return { cleaned: trimmed, isNegative }; +} + +function isValidForBase(val: string, base: number): boolean { + const { cleaned } = getCleanValue(val, base); + if (cleaned === "") return true; + + switch (base) { + case 10: + return /^[0-9]+$/.test(cleaned); + case 2: + return /^[01]+$/.test(cleaned); + case 16: + return /^[0-9a-fA-F]+$/.test(cleaned); + case 8: + return /^[0-7]+$/.test(cleaned); + default: + return false; + } +} + +function parseBigInt(str: string, base: number): bigint | null { + const { cleaned, isNegative } = getCleanValue(str, base); + if (cleaned === "") return null; + if (!isValidForBase(str, base)) return null; + + try { + let result: bigint; + if (base === 10) { + result = BigInt(cleaned); + } else if (base === 16) { + result = BigInt("0x" + cleaned); + } else if (base === 2) { + result = BigInt("0b" + cleaned); + } else if (base === 8) { + result = BigInt("0o" + cleaned); + } else { + return null; + } + return isNegative ? -result : result; + } catch { + return null; + } +} + +// ── Helper functions for formatting ────────────────────────────────────────── + +function formatDecimal(val: string, group: boolean): string { + if (!group || val === "" || val === "-") return val; + const isNeg = val.startsWith("-"); + const absVal = isNeg ? val.slice(1) : val; + const formatted = absVal.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return isNeg ? "-" + formatted : formatted; +} + +function formatBinary(val: string, group: boolean): string { + if (!group || val === "" || val === "-") return val; + const isNeg = val.startsWith("-"); + const absVal = isNeg ? val.slice(1) : val; + const reversed = absVal.split("").reverse().join(""); + const chunked = reversed.match(/.{1,4}/g); + if (!chunked) return val; + const formatted = chunked.join(" ").split("").reverse().join(""); + return isNeg ? "-" + formatted : formatted; +} + +function formatHex(val: string, group: boolean): string { + if (!group || val === "" || val === "-") return val.toUpperCase(); + const isNeg = val.startsWith("-"); + const absVal = isNeg ? val.slice(1) : val; + const reversed = absVal.split("").reverse().join(""); + const chunked = reversed.match(/.{1,2}/g); + if (!chunked) return val.toUpperCase(); + const formatted = chunked.join(" ").split("").reverse().join(""); + return (isNeg ? "-" + formatted : formatted).toUpperCase(); +} + +function formatOctal(val: string, group: boolean): string { + if (!group || val === "" || val === "-") return val; + const isNeg = val.startsWith("-"); + const absVal = isNeg ? val.slice(1) : val; + const reversed = absVal.split("").reverse().join(""); + const chunked = reversed.match(/.{1,3}/g); + if (!chunked) return val; + const formatted = chunked.join(" ").split("").reverse().join(""); + return isNeg ? "-" + formatted : formatted; +} + +// ── Main Component ──────────────────────────────────────────────────────────── + +export default function NumberBaseConverter() { + const [sourceValue, setSourceValue] = useState(""); + const [sourceBase, setSourceBase] = useState(10); + const [groupDigits, setGroupDigits] = useState(true); + + // Validate the active field value + const isValid = isValidForBase(sourceValue, sourceBase); + const parsedValue = isValid ? parseBigInt(sourceValue, sourceBase) : null; + + // Determine values to show in inputs + function getDisplayValue(base: number): string { + // If this is the active source field, display what the user is typing + if (sourceBase === base) { + return sourceValue; + } + + // If source is empty, negative-only, or invalid, display empty + if (!sourceValue || sourceValue === "-" || !isValid || parsedValue === null) { + return ""; + } + + // Convert the parsed BigInt value to the target base + const rawConverted = parsedValue.toString(base); + + // Apply grouping formatting + switch (base) { + case 10: + return formatDecimal(rawConverted, groupDigits); + case 2: + return formatBinary(rawConverted, groupDigits); + case 16: + return formatHex(rawConverted, groupDigits); + case 8: + return formatOctal(rawConverted, groupDigits); + default: + return rawConverted; + } + } + + // Get validation error message if the current source field has invalid input + function getValidationError(base: number): string | null { + if (sourceBase !== base || isValid || !sourceValue) { + return null; + } + + switch (base) { + case 10: + return "Invalid decimal number. Only digits 0-9 are allowed."; + case 2: + return "Invalid binary number. Only 0 and 1 are allowed."; + case 16: + return "Invalid hexadecimal number. Only digits 0-9 and letters A-F are allowed."; + case 8: + return "Invalid octal number. Only digits 0-7 are allowed."; + default: + return "Invalid characters for this base."; + } + } + + function handleInputChange(val: string, base: number) { + setSourceBase(base); + setSourceValue(val); + } + + function loadSample(val: string) { + setSourceBase(10); + setSourceValue(val); + } + + function clear() { + setSourceValue(""); + } + + const bases = [ + { base: 10, name: "Decimal", label: "Decimal (Base 10)", placeholder: "e.g. 255" }, + { base: 2, name: "Binary", label: "Binary (Base 2)", placeholder: "e.g. 11111111" }, + { base: 16, name: "Hexadecimal", label: "Hexadecimal (Base 16)", placeholder: "e.g. FF" }, + { base: 8, name: "Octal", label: "Octal (Base 8)", placeholder: "e.g. 377" }, + ]; + + return ( +
+ {/* Top Options Bar */} +
+ + +
+ Samples: + + + +
+
+ + {/* Grid Layout of Input Fields */} +
+ {bases.map(({ base, name, label, placeholder }) => { + const displayVal = getDisplayValue(base); + const errorMsg = getValidationError(base); + const hasVal = displayVal.length > 0 && displayVal !== "-"; + + return ( +
+ +
+ handleInputChange(e.target.value, base)} + className={`w-full rounded-xl border bg-surface p-4 pr-20 font-mono text-sm text-primary outline-none transition-all ${ + errorMsg + ? "border-danger focus:border-danger/80 focus:ring-1 focus:ring-danger/20" + : "border-border focus:border-accent/50 focus:ring-1 focus:ring-accent/10" + }`} + /> + {hasVal && !errorMsg && ( + + )} +
+ {errorMsg && ( +

+ {errorMsg} +

+ )} +
+ ); + })} +
+ + {/* Action Buttons & Note */} + + + + +
+

Features & Conversions:

+
    +
  • Supports base prefixes (e.g. typing 0x in hexadecimal, 0b in binary, or 0o in octal).
  • +
  • Arbitrary-precision integers using BigInt (handles very large numbers without overflow).
  • +
  • Supports negative values.
  • +
  • Local-only real-time conversion in your browser. No data is sent to external servers.
  • +
+
+
+ ); +} diff --git a/src/pages/tools/number-base-converter.astro b/src/pages/tools/number-base-converter.astro new file mode 100644 index 0000000..01248c1 --- /dev/null +++ b/src/pages/tools/number-base-converter.astro @@ -0,0 +1,20 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import NumberBaseConverter from "../../features/number-base-converter/NumberBaseConverter"; +--- + + +
+

+ Number Base Converter +

+

+ Convert numbers between Decimal, Binary, Hexadecimal, and Octal bases in real time. +

+
+ + +