DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

A base converter is two pure functions — Horner to read, repeated division to write — and one decision: BigInt, not Number

I built a base converter this week and my first instinct was that it needed clever math. It doesn't. A "number in base 16" and "the same number in base 2" aren't two different numbers — they're two spellings of one value, and the base only decides the digit alphabet and what each position is worth. So converting is two honest steps: read a string into one exact value, then write that value out in whatever base you asked for. Two pure functions, a guard, and one decision that actually matters. Here's the whole thing.

A digit's value is pure charCode arithmetic

Every base needs to know what one character is worth. '0''9' map to 0–9 and 'a''z' continue 10–35 — which is why base-36 is the natural ceiling (10 digits + 26 letters). Return -1 for anything that isn't a digit; the caller uses that to reject bad input.

function digitVal(ch){
  const c = ch.charCodeAt(0);
  if (c >= 48 && c <= 57)  return c - 48;        // '0'..'9' -> 0..9
  if (c >= 97 && c <= 122) return c - 97 + 10;   // 'a'..'z' -> 10..35
  return -1;                                     // not a digit
}
Enter fullscreen mode Exit fullscreen mode

Read a string with Horner's method

Parsing base-b is one line repeated: start at 0, and for each digit do acc = acc × b + digit. That's the entire positional-notation formula, evaluated left to right without ever computing a power. It validates as it goes — a digit < 0 or >= base isn't legal — and the critical choice is that acc is a BigInt, so it never rounds.

const B = BigInt(base);
let acc = 0n;                                    // BigInt, not 0
for (const ch of s){
  const d = digitVal(ch);
  if (d < 0 || d >= base) return { ok:false, error:`'${ch}' isn't base-${base}` };
  acc = acc * B + BigInt(d);                     // Horner step
}
Enter fullscreen mode Exit fullscreen mode

Write it back out with repeated division

The inverse of Horner: value modulo the base gives the last digit, integer-divide the base away, repeat until zero — collecting digits least-significant first, so prepend each one. Every operation stays in BigInt, so a 200-digit number is exactly as easy as 255.

while (v > 0n){
  out = DIGITS[Number(v % B)] + out;   // remainder = next digit (prepend)
  v = v / B;                            // BigInt division floors toward 0
}
Enter fullscreen mode Exit fullscreen mode

Why BigInt — the whole reason this tool exists

A JavaScript number is a 64-bit float. It holds integers exactly only up to 2⁵³; past that, consecutive integers start sharing one float and parseInt silently rounds. This is not a corner case — it's every 64-bit ID and hash you'll ever paste in.

parseInt("9007199254740993")     // 9007199254740992   off by one
Number("18446744073709551615")   // 18446744073709552000   rounded
BigInt("9007199254740993")       // 9007199254740993n   exact
Enter fullscreen mode Exit fullscreen mode

That one substitution is the difference between a toy and a correct converter.

One value, N views

The UI has no special logic. There's exactly one source of truth — the current BigInt — and every field is a view of it. Typing in a field parses its text in its own base; on success it becomes the new value and every other field re-renders with toBase. Skip the active field so the cursor doesn't jump; an invalid entry just marks that field red and leaves the value untouched.

function onEdit(field){
  const r = parseInBase(field.input.value, field.base);
  if (!r.ok){ markInvalid(field, r.error); return; }
  current = r.value;
  for (const other of FIELDS)
    if (other !== field) other.input.value = toBase(current, other.base);
}
Enter fullscreen mode Exit fullscreen mode

The arbitrary-base picker (2–36) needed no new code, because the engine was general from the start — base 10 and 16 were never hard-coded. Grouping into nibbles and bytes, the fixed-width fit check, and two's-complement (BigInt.asUintN(bits, v) gives you 0xFFFFFFD6 for −42) are thin garnish on top. The lesson underneath: find the single source of truth and make every view a pure function of it.

Type in any field and watch the rest re-spell the same value:
https://dev48v.infy.uk/solve/day47-number-base-converter.html

Top comments (0)