Why Your Hex Converter Lies to You: Two's Complement, the 53-Bit Wall, and Radix Magic
An in-depth exploration of two's complement, sign extension traps, IEEE-754 precision limits in JavaScript, and why Radix 36 is the superior choice for URL-safe
In the world of programming, every developer eventually encounters a puzzling moment during debugging: casting a signed 8-bit -1 to hexadecimal unexpectedly yields 0xFF. When widened to a 32-bit register, it expands into 0xFFFFFFFF, seemingly erasing the negative sign. Additionally, running 0x20000000000001 through a JavaScript parser produces an incorrect integer.
While number base conversion is often seen as a simple Computer Science task, radix shifts are deeply connected to hardware registers, two's complement mathematics, IEEE-754 rounding traps, and data-encoding strategies. Understanding these low-level mechanics is crucial to avoid catastrophic bugs in production.
1. The Illusion of the Minus Sign: Two's Complement
Hardware processors lack dedicated cells for storing an aesthetically pleasing minus sign. Instead, negative numbers are encoded via Two's Complement in CPU registers. In an NNN-bit register, the most significant bit (MSB) serves as a negative weight. For example, assigning -1 to an 8-bit signed integer results in 1111 1111 in hexadecimal (0xFF). When widening the integer to 32 bits, sign extension occurs, duplicating the MSB (1) across all newly allocated upper bits, preserving the mathematical value of -1.
2. Shift Operations: Arithmetic vs. Logical
Understanding two's complement helps clarify the difference between bitwise shift operators. Arithmetic Right Shift (>>) preserves the sign bit, filling vacated positions on the left with the MSB's value. Logical Right Shift (<<) is not sign-aware, always filling vacated positions with zeros. Accidentally using one shift operation over the other can lead to bugs that remain undetected with small positive integers but cause issues in telemetry, hashing routines, and protocol deserialization.
3. The 53-Bit Wall: JavaScript's Silent Corruption
JavaScript's Number type adheres to the IEEE-754 double-precision floating-point standard, allocating 1 sign bit, 11 exponent bits, and 52 explicit fraction bits (53 bits of precision with an implicit leading 1). Integers larger than Number.MAX_SAFE_INTEGER (253−1, or 9,007,199,254,740,991) cannot be precisely represented. Converting a large hexadecimal hash or a 64-bit Snowflake ID using standard floating-point numbers results in the loss of the least significant bits due to rounding.
The solution is to use BigInt, which allocates arbitrary-precision integer chunks in heap arrays, preserving exact values.
4. Radix 36: URL Slugs and Distributed IDs
While binary, octal, and hexadecimal map directly onto CPU word architectures, higher non-power-of-two radices serve specialized purposes. Radix 36 (0-9, a-z) is particularly useful for URL slugs and distributed identifiers. It offers a balance between raw density and ease of readability, making it ideal for generating case-insensitive identifiers that work across various systems. Unlike Base-64, Radix 36 is URL-safe and avoids collisions on case-insensitive filesystems and databases.
Written by urgent.news from HackerNoon's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.