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:

  1. Repeatedly divide the decimal number by the target base
  2. Use remainders as digit values
  3. Continue with the quotient
  4. 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