Converting between number bases
Write a number in another base by taking remainders, and read it back by multiplying and adding digit by digit.
O(log n)Space O(log n)3 practice problemsStep-through pictureThe idea
We write numbers in base 10: the digits of 156 mean 1 × 100 + 5 × 10 + 6 × 1. Each place is worth 10 times the place to its right. Other bases work the same way with a different multiplier. In base 16 (hexadecimal), each place is worth 16 times the one to its right, and the digits go 0-9 then a-f for 10 to 15. In base 2 (binary), the places are worth 1, 2, 4, 8, ....
To write a number in a base, take rest % base: that's the rightmost digit. Then divide rest by the base (rounding down) and repeat to get the next digit to the left. Stop when rest is 0.
To read a number written in a base, go left to right: value = value * base + digit. Each step shifts what you have one place to the left and adds the new digit.
An everyday example
Packing eggs into boxes of boxes
You have 156 eggs. Cartons hold 16 eggs, and crates hold 16 cartons. 156 eggs fill 9 cartons with 12 eggs left over. 9 cartons don't fill a crate. So you have 0 crates, 9 cartons, and 12 loose eggs: in base 16, that's 9c, since c stands for 12. Each step asks "how many are left over after packing into the next size up?"
| In the example | In the code |
|---|---|
| Loose eggs left after filling cartons | rest % base |
| The number of full cartons | Math.floor(rest / base) |
| Writing the leftover count on the label | out = DIGITS[rest % base] + out |
| Unpacking: cartons times 16 plus loose eggs | value = value * base + DIGITS.indexOf(ch) |
From the slow way to converting between number bases
- Start with the way people often do it by hand: to write
2989in base 16, find the largest power of 16 that fits (16^2 = 256), count how many times it fits (11times, the digitb), subtract, and move on to16^1, then16^0. It works, but you must first find the right power, and each step divides by a different number. - Notice which digit is easiest to get: the last one. Every place except the last is worth a multiple of the base, so
n % baseis exactly the last digit. For2989 % 16that's13, the digitd, with no powers needed. - To get the next digit, throw the last one away.
Math.floor(n / base)drops the last digit and moves every other digit one place to the right, so the next% basegives the next digit. Repeat until nothing is left. The digits come out from right to left, so each new one goes in front ofout. - Reading a number back runs the same idea the other way. Going left to right,
value * baseshifts what you have one place to the left, and+ digitfills the new last place. Handle0and the sign before the loop, keep the digit symbols in oneDIGITSstring, and you have two short loops. Those loops are base conversion.
How it works
- Keep the digit symbols in order in a string:
DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz". - To write
ninbase: handle0and the sign first, then setrest = Math.abs(n). - While
rest > 0, putDIGITS[rest % base]in front ofout, then setrest = Math.floor(rest / base). - Return the sign followed by
out. - To read
textinbase: start withvalue = 0and, for each character left to right, setvalue = value * base + DIGITS.indexOf(ch).
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz";function toBase(n, base) {if (n === 0) return "0";const sign = n < 0 ? "-" : "";let rest = Math.abs(n);let out = "";while (rest > 0) {out = DIGITS[rest % base] + out;rest = Math.floor(rest / base);}return sign + out;}function fromBase(text, base) {let value = 0;for (const ch of text.toLowerCase()) {value = value * base + DIGITS.indexOf(ch);}return value;}
How fast it is
| Time | O(log n) |
|---|---|
| Extra memory | O(log n) |
Each step produces one digit, and n has about log_base n digits. The output string holds those digits.
- Best case
O(1)nis 0, which returns at once, ornis smaller thanbase, which needs one loop step. - Average case
O(log n)One step per digit, and
nhas aboutlog_base ndigits:2989has 3 digits in base 16. - Worst case
O(log n)Base 2, which gives the most digits:
2989needs 12 steps. Adding each digit to the front copiesout, but with at most 53 digits that cost stays tiny.
Common mistakes
- Appending each digit to the end instead of the front. The digits come out from right to left, so the result is reversed.
- Forgetting
0, which makes the loop return an empty string. - Using
parseIntwithout a base, likeparseInt("08"). Always pass the base:parseInt(text, 10). - Using plain numbers for long hex strings, such as a 20-character ID. Values past
2^53lose precision; useBigInt("0x" + text)instead.
When to use it
Look for these clues in a problem:
- The input or output is in hex, binary, octal, or base 36, or uses letters as digits.
- Colors like
#1a2b3c, short IDs, or spreadsheet column names likeABneed to become numbers or back. - The problem asks for the digits of a number in some base, or for a sum of those digits.
- Numbers are given as strings because they're too long or use an unusual alphabet.
Check yourself
Answer each one in your head first, then open it.
What does toBase(255, 16) return?
"ff". 255 % 16 = 15, which is f, then rest = 15, and 15 % 16 is also f.
Why must each new digit go in front of out?
The loop finds the last digit first. Adding to the end would build the digits in reverse order: "dab" instead of "bad".
How many loop steps does toBase(2989, 2) take?
12, because 2989 is 101110101101 in binary, which has 12 digits. One step per digit.
Why are fromBase's middle values the same as toBase's rest values in reverse?
Each rest is the number spelled by the digits not yet written out, which are the leading digits. fromBase builds exactly those leading digits, shortest first.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
toBase keeps one promise at the top of every loop: Math.abs(n) equals rest × base^L plus the value of the digits in out, where L is the length of out. At the start out is empty and rest is Math.abs(n), so it's true.
Each step keeps it. Write rest = q × base + r with r = rest % base. Putting the digit for r in front of out and setting rest = q turns (q × base + r) × base^L + out into q × base^(L + 1) + (r × base^L + out), which is the same number. Since r is between 0 and base - 1, it is always a valid single digit.
The loop ends because rest shrinks by a factor of at least base (at least 2) each time, so it hits 0 after one step per digit. Then the promise says Math.abs(n) equals the value of out. fromBase keeps the matching promise: after reading some characters, value is the number those characters spell, because multiplying by base moves every digit one place left and adding the new digit fills the last place.
Going deeper
3 more ideas
Why remainders give the digits from right to left
A number written in base b with digits d2 d1 d0 equals d2 × b² + d1 × b + d0. Every part except d0 is a multiple of b, so n % b is d0, the last digit. Dividing by b and rounding down removes d0 and shifts everything one place right, so the next remainder is d1.
JavaScript's built-ins
(156).toString(16) gives "9c" and parseInt("9c", 16) gives 156, for bases 2 to 36. They're the right choice in real code. Write the loop yourself when the alphabet is unusual (like base 62 with uppercase letters, or spreadsheet columns where A is 1 and there's no zero digit), or when numbers pass 2^53 and need BigInt.
The cost
A number n has about log_base(n) digits, and each loop step produces one, so converting takes O(log n) steps. Reading a string of L characters takes O(L) steps.
Work it by hand
Every step in a table, with why
Write -2989 in base 16, then read the digits back with fromBase.
toBase(-2989, 16), then fromBase("BAD", 16)
| Step | rest or value | Digit | out or ch | What happens | Why |
|---|---|---|---|---|---|
| 1 | 2989 | - | "" | sign = "-" | n isn't 0. It's negative, so sign is "-" and rest becomes Math.abs(-2989) = 2989. |
| 2 | 2989 | 13 (d) | "d" | rest = 186 | 2989 % 16 = 13, the symbol d. 2989 = 186 × 16 + 13, so rest becomes 186. |
| 3 | 186 | 10 (a) | "ad" | rest = 11 | 186 % 16 = 10, the symbol a. It goes in front of d. |
| 4 | 11 | 11 (b) | "bad" | rest = 0 | 11 % 16 = 11, the symbol b. Math.floor(11 / 16) is 0. |
| 5 | 0 | - | "bad" | return "-bad" | rest is 0, so every digit is out. The sign goes in front. |
| 6 | 0 | - | - | fromBase starts | value = 0. text.toLowerCase() turns "BAD" into "bad", so upper and lower case both work. |
| 7 | 11 | 11 | b | 0 × 16 + 11 | DIGITS.indexOf("b") is 11. |
| 8 | 186 | 10 | a | 11 × 16 + 10 | Shifting 11 one place left gives 176; adding a gives 186. |
| 9 | 2989 | 13 | d | 186 × 16 + 13 | Shifting 186 gives 2976; adding d gives 2989. Every character is read. |
toBase returns "-bad" and fromBase returns 2989. The values 11, 186, 2989 that fromBase builds are exactly the rest values of toBase, in reverse order.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Converting between number bases | O(log n) | O(log n) | An unusual alphabet, a base above 36, spreadsheet-style columns, or BigInt values. |
Built-in toString(base) and parseInt(text, base) | O(log n) | O(log n) | Standard bases from 2 to 36 in real code: they're tested, fast, and short. |
| Largest power first | O(log n) | O(log n) | Working by hand, or when digits must come out from left to right as a stream. |
Extra memory: O(log n): the output string has one character per digit. Only rest and sign are kept besides it.
How the steps grow
- Converting between number bases, Built-in
toString(base)andparseInt(text, base), Largest power firstO(log n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Converting between number bases | 3 | 10 | 20 |
Built-in toString(base) and parseInt(text, base) | 3 | 10 | 20 |
| Largest power first | 3 | 10 | 20 |
Inputs that need care
5 tricky inputs
n = 0- The first line returns
"0". Without it, the loop wouldn't run and the result would be an empty string. - A negative number:
toBase(-2989, 16) - The sign is saved, the digits are computed from
Math.abs(n), and the result is"-bad". ButfromBase("-bad", 16)returns-1107, becauseindexOf("-")is-1. Strip the sign before reading. - A character that isn't a digit of the base:
fromBase("12z", 10) zhas index 35, so it returns155without complaint. Check that every index is from 0 tobase - 1, and reject the text otherwise.- A decimal:
toBase(3.5, 2) 3.5 % 2is1.5, andDIGITS[1.5]isundefined, so it returns"1undefined". The code is for whole numbers only.- A number past
2^53:toBase(2 ** 53 + 1, 16) - The
+ 1is lost before the call even starts, so it returns"20000000000000", which is2^53. UseBigIntfor IDs and hashes this large.
Variations and follow-ups
Twists interviewers add
Spreadsheet columns
Column names use A to Z for 1 to 26 and have no zero digit. Subtract 1 before each % 26: n--, then take the letter for n % 26, then n = Math.floor(n / 26). Column 28 is AB, and 703 is AAA.
Your own alphabet
Short links often use base 62 with 0-9, a-z, and A-Z. Only the DIGITS string changes. The loops are the same, and the built-in toString can't do it because it stops at base 36.
BigInt IDs
The same loops work with BigInt values, using rest % 16n and rest / 16n. That's how you convert a 128-bit value, such as a UUID's hex digits, without losing precision.
Collect digits in an array
Instead of adding each digit to the front of a string, push it onto an array and reverse().join("") once at the end. It avoids copying the string on every step, which only matters for very long outputs.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "convert to binary or hex", "spreadsheet column title", "add two binary strings", or any representation where each place is worth a fixed multiple of the next.
- Explain both directions in one breath: "
% basegives the last digit andMath.floor(/ base)removes it, so digits come out right to left. Reading back,value * base + digitadds one digit on the right." - Test
0, a negative number, a number smaller than the base, and the largest single digit, liketoBase(35, 36), which is"z", againsttoBase(36, 36), which is"10". - The usual follow-up is spreadsheet columns, where there's no zero digit. Explain the
n--before each step, and check it onZ(26) andAA(27).
Where frontend code uses it
The same idea in real UI code
- CSS hex colors like
#1a2b3care three base-16 numbers:1a = 26,2b = 43,3c = 60. Color pickers convert between hex andrgb()this way. - Short IDs and URL shorteners often write a number in base 36 or base 62 so it uses fewer characters.
Date.now().toString(36)is a common quick ID. - Spreadsheet-like data grids label columns
A,B, ...,Z,AA,AB: a base-26 system without a zero digit.