JavaScript Number Base Converter: From parseInt to Bitwise Operations#

Published: April 29, 2026, 11:00

Last week while debugging a network protocol, I found myself constantly converting numbers between binary, octal, decimal, and hexadecimal formats. Opening the calculator every time was tedious, so I decided to build an online number base converter. This seemingly simple tool has some interesting technical details worth sharing.

The Core Principle of Base Conversion#

Base conversion is fundamentally about positional notation. In base-N, each position has a weight of N raised to a power. For example, decimal 123 is actually:

1 × 10² + 2 × 10¹ + 3 × 10⁰ = 100 + 20 + 3 = 123

The same number in binary is 1111011:

1×2⁶ + 1×2⁵ + 1×2⁴ + 1×2³ + 0×2² + 1×2¹ + 1×2⁰ = 64+32+16+8+0+2+1 = 123

Once you understand this principle, the conversion algorithm becomes clear: any base → decimal → target base.

JavaScript Built-in Base Conversion#

JavaScript provides two native methods for base conversion:

parseInt: Any Base to Decimal#

// Binary to decimal
parseInt('1010', 2)  // 10

// Octal to decimal
parseInt('12', 8)    // 10

// Hexadecimal to decimal
parseInt('A', 16)    // 10

The second parameter of parseInt is the radix, ranging from 2 to 36. Why 36? Because 0-9 plus A-Z gives exactly 36 characters.

toString: Decimal to Any Base#

// Decimal to binary
(10).toString(2)   // "1010"

// Decimal to octal
(10).toString(8)   // "12"

// Decimal to hexadecimal
(10).toString(16)  // "a"

Note that toString returns a string, and hexadecimal defaults to lowercase letters. Use .toUpperCase() if you need uppercase.

Building a Complete Base Converter#

Based on these two methods, we can quickly implement a converter:

class RadixConverter {
  constructor() {
    this.supportedRadix = [2, 8, 10, 16]
  }

  // Validate input
  validate(value, radix) {
    if (!value) return true
    const chars = '0123456789ABCDEF'.slice(0, radix)
    const regex = new RegExp(`^[${chars}]+$`, 'i')
    return regex.test(value)
  }

  // Convert from any base
  convertFrom(value, fromRadix) {
    if (!this.validate(value, fromRadix)) {
      throw new Error(`Invalid ${fromRadix}-base number: ${value}`)
    }
    
    const decimal = parseInt(value.toUpperCase(), fromRadix)
    
    return {
      binary: decimal.toString(2),
      octal: decimal.toString(8),
      decimal: decimal.toString(10),
      hex: decimal.toString(16).toUpperCase()
    }
  }

  // Batch conversion
  batchConvert(decimal) {
    return {
      binary: decimal.toString(2),
      octal: decimal.toString(8),
      decimal: decimal.toString(10),
      hex: decimal.toString(16).toUpperCase()
    }
  }
}

// Usage example
const converter = new RadixConverter()
console.log(converter.convertFrom('FF', 16))
// { binary: "11111111", octal: "377", decimal: "255", hex: "FF" }

Performance Optimization: The Power of Bitwise Operations#

While parseInt and toString are convenient, they may not be fast enough when processing large amounts of data. This is where bitwise operations can help.

Fast Binary Conversion#

// Decimal to binary (bitwise version)
function decimalToBinary(num) {
  if (num === 0) return '0'
  
  let binary = ''
  while (num > 0) {
    binary = (num & 1) + binary  // Get lowest bit
    num = num >>> 1              // Unsigned right shift
  }
  return binary
}

// Performance comparison
console.time('toString')
for (let i = 0; i < 100000; i++) {
  (123456789).toString(2)
}
console.timeEnd('toString')  // ~50ms

console.time('bitwise')
for (let i = 0; i < 100000; i++) {
  decimalToBinary(123456789)
}
console.timeEnd('bitwise')  // ~30ms

The bitwise version is about 40% faster! But note JavaScript’s bitwise operation limitations:

  1. Only supports 32-bit integers: Numbers larger than 2³¹-1 will be truncated
  2. Signed number handling: Negative numbers need special treatment

Fast Hexadecimal Conversion#

// Decimal to hexadecimal (lookup table)
function decimalToHex(num) {
  const hexChars = '0123456789ABCDEF'
  if (num === 0) return '0'
  
  let hex = ''
  while (num > 0) {
    hex = hexChars[num & 0xF] + hex  // Get low 4 bits
    num = num >>> 4                  // Right shift 4 bits
  }
  return hex
}

console.log(decimalToHex(255))  // "FF"
console.log(decimalToHex(4095)) // "FFF"

The lookup table approach is about 25% faster than toString(16) and gives precise control over output format.

The Big Number Trap#

JavaScript’s Number type is IEEE 754 double-precision floating-point, with a maximum safe integer of 2⁵³-1 = 9007199254740991. Beyond this, precision is lost:

// Precision loss example
const bigNum = 9007199254740993
console.log(bigNum.toString(16))  // "20000000000001"
console.log((bigNum + 1).toString(16))  // "20000000000001" (same!)

// Solution: Use BigInt
const bigNumInt = BigInt('9007199254740993')
console.log(bigNumInt.toString(16))  // "20000000000002"

BigInt Base Converter#

class BigIntRadixConverter {
  convertFrom(value, fromRadix) {
    const decimal = BigInt('0x' + this.toHex(value, fromRadix))
    
    return {
      binary: decimal.toString(2),
      octal: decimal.toString(8),
      decimal: decimal.toString(10),
      hex: decimal.toString(16).toUpperCase()
    }
  }

  // Helper: any base to hex string
  toHex(value, radix) {
    // ... implementation omitted
  }
}

Real-World Applications#

1. Color Code Conversion#

// RGB to HEX
function rgbToHex(r, g, b) {
  return '#' + [r, g, b]
    .map(x => x.toString(16).padStart(2, '0'))
    .join('')
    .toUpperCase()
}

console.log(rgbToHex(255, 0, 0))  // "#FF0000"

// HEX to RGB
function hexToRgb(hex) {
  const decimal = parseInt(hex.replace('#', ''), 16)
  return {
    r: (decimal >> 16) & 0xFF,
    g: (decimal >> 8) & 0xFF,
    b: decimal & 0xFF
  }
}

console.log(hexToRgb('#FF0000'))  // { r: 255, g: 0, b: 0 }

2. File Permission Calculation#

// Octal permission to symbolic representation
function permissionToString(octal) {
  const perms = parseInt(octal, 8)
  const rwx = ['r', 'w', 'x']
  
  return ['owner', 'group', 'other']
    .map((_, i) => {
      const shift = 6 - i * 3
      return rwx.map((p, j) => 
        (perms >> (shift - j)) & 1 ? p : '-'
      ).join('')
    }).join('')
}

console.log(permissionToString('755'))  // "rwxr-xr-x"
console.log(permissionToString('644'))  // "rw-r--r--"

3. IP Address Calculation#

// IP address to integer
function ipToInt(ip) {
  return ip.split('.')
    .reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0
}

// Integer to IP address
function intToIp(int) {
  return [
    (int >>> 24) & 0xFF,
    (int >>> 16) & 0xFF,
    (int >>> 8) & 0xFF,
    int & 0xFF
  ].join('.')
}

console.log(ipToInt('192.168.1.1'))  // 3232235777
console.log(intToIp(3232235777))     // "192.168.1.1"

Common Pitfalls and Solutions#

1. Leading Zero Problem#

// Wrong: Octal literal (error in strict mode)
const num = 010  // ES5 strict mode error

// Correct: Explicitly specify base
const num = parseInt('10', 8)  // 8

2. Negative Number Handling#

// toString doesn't support negative bases
(-10).toString(2)  // "-1010"

// Manual two's complement implementation needed
function toTwosComplement(num, bits = 32) {
  if (num >= 0) return num.toString(2).padStart(bits, '0')
  
  const positive = (-num).toString(2).padStart(bits, '0')
  const inverted = positive.split('').map(b => b === '0' ? '1' : '0').join('')
  const decimal = parseInt(inverted, 2) + 1
  return decimal.toString(2).padStart(bits, '0')
}

console.log(toTwosComplement(-10, 8))  // "11110110"

3. Floating-Point Base Conversion#

// Floating-point decimal to binary
function floatToBinary(num) {
  const intPart = Math.floor(num)
  const fracPart = num - intPart
  
  let binary = intPart.toString(2) + '.'
  
  // Fractional part: multiply by 2 repeatedly
  let frac = fracPart
  for (let i = 0; i < 23 && frac > 0; i++) {
    frac *= 2
    binary += Math.floor(frac)
    frac -= Math.floor(frac)
  }
  
  return binary
}

console.log(floatToBinary(10.625))  // "1010.101"

Conclusion#

Base conversion seems simple, but deep implementation reveals various edge cases:

  1. Big number precision: Use BigInt for numbers exceeding 2⁵³-1
  2. Performance optimization: Bitwise operations are 25-40% faster than toString
  3. Negative number handling: Two’s complement representation needs manual implementation
  4. Floating-point numbers: Fractional parts require special algorithms

The online number base converter I built integrates these optimizations, supporting real-time conversion between binary, octal, decimal, and hexadecimal, with quick examples for common values. Next time you need base conversion, give this tool a try.