Understanding Radix Conversion: From Binary to Hexadecimal Algorithms
Understanding Radix Conversion: From Binary to Hexadecimal Algorithms#
While building a radix converter tool, I revisited the fundamentals of number base conversion. Although parseInt and toString handle most cases, understanding the underlying algorithms is crucial for dealing with edge cases.
JavaScript Built-in Conversion#
The simplest approach uses JavaScript’s built-in methods. Number.prototype.toString(radix) converts decimal to any base:
const num = 255
num.toString(2) // "11111111" (binary)
num.toString(8) // "377" (octal)
num.toString(16) // "ff" (hexadecimal)
Reverse conversion uses parseInt(string, radix):
parseInt('11111111', 2) // 255
parseInt('377', 8) // 255
parseInt('ff', 16) // 255
These APIs support bases 2-36 (digits + letters = 36 characters max). But real-world implementations need more consideration.
Decimal to Base N Algorithm#
The core logic behind toString(radix) is the division-remainder method. Here’s a manual implementation:
function decimalToBase(num: number, base: number): string {
if (num === 0) return '0'
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let result = ''
while (num > 0) {
const remainder = num % base
result = digits[remainder] + result
num = Math.floor(num / base)
}
return result
}
decimalToBase(255, 2) // "11111111"
decimalToBase(255, 16) // "FF"
Algorithm core:
- Repeatedly divide the decimal number by the target base
- Use remainders as digit values
- Continue with the quotient
- Read remainders bottom-up for the result
For example, converting 255 to binary:
255 ÷ 2 = 127 remainder 1
127 ÷ 2 = 63 remainder 1
63 ÷ 2 = 31 remainder 1
31 ÷ 2 = 15 remainder 1
15 ÷ 2 = 7 remainder 1
7 ÷ 2 = 3 remainder 1
3 ÷ 2 = 1 remainder 1
1 ÷ 2 = 0 remainder 1
Reading remainders from bottom to top: 11111111
Base N to Decimal Algorithm#
The reverse operation uses positional notation expansion. Multiply each digit by its positional weight (base^position), then sum:
function baseToDecimal(str: string, base: number): number {
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let result = 0
for (let i = 0; i < str.length; i++) {
const char = str[i].toUpperCase()
const value = digits.indexOf(char)
if (value === -1 || value >= base) {
throw new Error(`Invalid digit ${char} for base ${base}`)
}
result = result * base + value
}
return result
}
baseToDecimal('FF', 16) // 255
baseToDecimal('11111111', 2) // 255
Calculation breakdown:
FF (hex) = 15×16¹ + 15×16⁰ = 240 + 15 = 255
11111111 (binary) = 1×2⁷ + 1×2⁶ + ... + 1×2⁰ = 255
Note: result = result * base + value avoids redundant power calculations, improving performance.
Arbitrary Base Conversion#
With these two functions, converting between any bases becomes a composition:
function convertBase(str: string, fromBase: number, toBase: number): string {
const decimal = baseToDecimal(str, fromBase)
return decimalToBase(decimal, toBase)
}
convertBase('FF', 16, 2) // "11111111"
convertBase('377', 8, 16) // "FF"
Of course, using JavaScript’s built-in APIs is simpler:
function convertBase(str: string, fromBase: number, toBase: number): string {
const decimal = parseInt(str, fromBase)
return decimal.toString(toBase).toUpperCase()
}
But real-world tools need to handle more edge cases.
Input Validation and Error Handling#
A radix converter must validate input. Binary inputs can’t have digits 2-9, hexadecimal can’t have G-Z.
function validateInput(value: string, radix: number): boolean {
if (!value) return true
const chars = '0123456789ABCDEF'.slice(0, radix)
const regex = new RegExp(`^[${chars}]+$`, 'i')
return regex.test(value)
}
validateInput('10102', 2) // false (binary can't have 2)
validateInput('GG', 16) // false (hex can't have G)
validateInput('12AB', 16) // true
The trick: dynamically generate allowed character sets based on radix. Binary is 01, octal is 01234567, hexadecimal is 0123456789ABCDEF.
Large Number Precision#
JavaScript’s Number type is IEEE 754 double-precision float. The maximum safe integer is 2^53 - 1 (9007199254740991). Larger integers lose precision:
const bigNum = 9007199254740993
console.log(bigNum.toString(16)) // "20000000000000" (incorrect)
Solution: use BigInt:
function bigDecimalToBase(num: bigint, base: number): string {
if (num === 0n) return '0'
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let result = ''
let n = num
while (n > 0n) {
const remainder = Number(n % BigInt(base))
result = digits[remainder] + result
n = n / BigInt(base)
}
return result
}
const bigNum = BigInt('9007199254740993')
console.log(bigDecimalToBase(bigNum, 16)) // "20000000000001" (correct)
Fractional Number Conversion (Advanced)#
Integer conversion is straightforward, but fractional parts are more complex. Converting decimal fractions to binary uses multiplication method:
function decimalFractionToBinary(num: number, precision = 10): string {
let result = ''
let n = num
for (let i = 0; i < precision; i++) {
n = n * 2
if (n >= 1) {
result += '1'
n = n - 1
} else {
result += '0'
}
}
return '0.' + result
}
decimalFractionToBinary(0.5) // "0.1000000000"
decimalFractionToBinary(0.1) // "0.0001100110" (repeating)
Key insight: many decimal fractions are infinite repeating in binary. For example, 0.1 in binary is 0.0001100110011... repeating infinitely. This is the root cause of floating-point precision issues.
Practical Applications#
In web development, radix conversion is commonly used for:
1. Color Value Conversion#
RGB to HEX color:
function rgbToHex(r: number, g: number, b: number): string {
return '#' + [r, g, b]
.map(x => x.toString(16).padStart(2, '0'))
.join('')
}
rgbToHex(255, 0, 0) // "#ff0000"
2. Unix Permission Calculation#
File permission 755 is actually octal:
const permission = '755'
const binary = parseInt(permission, 8).toString(2)
// "111101101" corresponds to rwxr-xr-x
3. Base64 Encoding#
Base64 essentially converts binary data to base-64 representation:
const text = 'Hello'
const base64 = btoa(text) // "SGVsbG8="
Base64 uses 64 characters (A-Z, a-z, 0-9, +, /), equivalent to base-64.
Performance Optimization#
For batch conversions, caching common results improves performance:
const cache = new Map<string, string>()
function cachedConvert(str: string, fromBase: number, toBase: number): string {
const key = `${str}-${fromBase}-${toBase}`
if (cache.has(key)) {
return cache.get(key)!
}
const result = convertBase(str, fromBase, toBase)
cache.set(key, result)
return result
}
For frequent conversions (like color values), caching provides significant performance gains.
Complete Implementation#
Combining all considerations, a complete radix converter implementation:
export function useRadixConverter() {
const [values, setValues] = useState({
binary: '',
octal: '',
decimal: '',
hex: '',
})
const convertFromDecimal = (decimal: number) => {
return {
binary: decimal.toString(2),
octal: decimal.toString(8),
decimal: decimal.toString(10),
hex: decimal.toString(16).toUpperCase(),
}
}
const handleChange = (type: string, value: string) => {
const radixMap: Record<string, number> = {
binary: 2,
octal: 8,
decimal: 10,
hex: 16,
}
const radix = radixMap[type]
const decimal = parseInt(value, radix)
if (!isNaN(decimal) && decimal >= 0) {
setValues(convertFromDecimal(decimal))
}
}
return { values, handleChange }
}
This implementation is concise and practical, covering most radix conversion needs in daily development.
Summary#
Radix conversion seems basic but involves many details:
- Division-remainder and positional notation are core algorithms
- Input validation ensures data integrity
- BigInt handles large number precision
- Fractional conversion has repeating decimal traps
Understanding these principles helps handle real-world conversion scenarios better. Online tool: Radix Converter, supporting real-time conversion between binary, octal, decimal, and hexadecimal.
Related tools: Base64 Encoder/Decoder | Hash Generator