Strings · String Fundamentals

String Symbol Mapping

Converting between a value and its symbolic representation using a fixed lookup table of symbol/value pairs, ordered from largest to smallest so a greedy left-to-right pass produces the correct encoding (or decoding) in one direction or the other.

  • 5 min read
  • 9 practice questions
  • 3 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: a positional system with no zero
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Converting between a number and its written form, such as Roman numerals, spreadsheet column letters or "2h 5m", is a table plus a loop. The table is the easy part. Subtractive pairs, a numbering system with no zero digit, and input that isn't in its standard form are where these conversions go wrong. This skill covers greedy encoding, lookahead decoding, base 26 with no zero, and how to validate what you decode.

We'll go from simple to more technical:

  1. The pattern: greedy encoding from a value table, and decoding with one symbol of lookahead.
  2. A second flavor: spreadsheet columns, base 26 with no zero.
  3. Where symbol mappings show up (spreadsheets, versions, durations, IDs).
  4. What breaks it: missing pairs, the last symbol, no zero digit, untrusted input.

The approach

Java

int romanToInt(String s) {
    Map<Character, Integer> value = Map.of('I', 1, 'V', 5, 'X', 10, 'L', 50, 'C', 100, 'D', 500, 'M', 1000);
    int total = 0;
    for (int i = 0; i < s.length(); i++) {
        int cur = value.get(s.charAt(i));
        if (i + 1 < s.length() && cur < value.get(s.charAt(i + 1))) total -= cur;   // the I in IV
        else total += cur;
    }
    return total;
}

String intToRoman(int num) {
    int[] values =     {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
    String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < values.length; i++) {
        while (num >= values[i]) { num -= values[i]; sb.append(symbols[i]); }   // greedy: largest first
    }
    return sb.toString();
}

Python

def roman_to_int(s: str) -> int:
    value = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
    total = 0
    for i, ch in enumerate(s):
        cur = value[ch]
        if i + 1 < len(s) and cur < value[s[i + 1]]:
            total -= cur                             # the I in IV
        else:
            total += cur
    return total


def int_to_roman(num: int) -> str:
    table = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
             (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
    parts = []
    for value, symbol in table:
        while num >= value:                          # greedy: largest first
            num -= value
            parts.append(symbol)
    return "".join(parts)

Decoding needs one look ahead: a symbol smaller than the next one is subtracted, and the last symbol has no next one (hence the bounds check). Encoding is greedy - take the largest value that fits, repeatedly - and putting the six subtractive pairs in the table as their own entries is what keeps it a plain loop. Leave out CD, XL and IV and 444 comes out as CCCCXXXXIIII. Greedy is correct here because the table is canonical, like coins 1, 5, 10, 25; with coins 1, 3 and 4, greedy pays 6 as 4 + 1 + 1 instead of 3 + 3.

A second flavor: a positional system with no zero

Spreadsheet columns run A..Z, then AA, AB, ...: base 26 with digits 1 to 26 and no zero ("bijective" base 26). Reading one is ordinary positional decoding (Horner's rule); writing one needs a shift:

Java

int titleToNumber(String t) {                   // "AB" -> 1 * 26 + 2 = 28
    int v = 0;
    for (char c : t.toCharArray()) v = v * 26 + (c - 'A' + 1);
    return v;
}

String convertToTitle(int n) {                  // 28 -> "AB"
    StringBuilder sb = new StringBuilder();
    while (n > 0) {
        n--;                                    // map 1..26 to 0..25 before taking the digit
        sb.append((char) ('A' + n % 26));
        n /= 26;
    }
    return sb.reverse().toString();
}

Python

def title_to_number(t: str) -> int:                  # "AB" -> 1 * 26 + 2 = 28
    v = 0
    for ch in t:
        v = v * 26 + (ord(ch) - ord("A") + 1)
    return v


def convert_to_title(n: int) -> str:                 # 28 -> "AB"
    letters = []
    while n > 0:
        n, r = divmod(n - 1, 26)                     # map 1..26 to 0..25 before taking the digit
        letters.append(chr(ord("A") + r))
    return "".join(reversed(letters))

Without the n--, 26 has remainder 0, which has no letter; decrementing only inside the letter calculation ((n - 1) % 26) but dividing the original n turns 26 into "AZ". The largest Excel column, XFD, is 24 * 676 + 6 * 26 + 4 = 16,384.

Where you'll see it

Formatting sizes and durations ("1.5 GB", "2h 5m") greedily takes the largest unit that fits; spreadsheet and report column labels use bijective base 26; short-URL and ID schemes encode numbers in base 62 (which does have a zero digit). Any closed set of codes (status codes to labels, country codes to names) is the same table lookup.

Which tool for which job?

You needUse
Number to Romangreedy over a 13-entry table, including CM, CD, XC, XL, IX, IV
Roman to numbersubtract a symbol smaller than the next one
Is a numeral valid?decode, re-encode, and compare
Column number and lettersbase 26 with no zero: subtract 1 before each digit
Other basesInteger.toString(n, radix) / int(s, base)
DurationsISO-8601 (Duration.parse("PT2H5M"))

Common mistakes

MistakeWhat happensDo this instead
A table without the subtractive pairs444 becomes CCCCXXXXIIIIinclude CM, CD, XC, XL, IX, IV
Looking past the last symbolindex out of rangethe last symbol has no lookahead
Treating columns as base 26 with a zero26 becomes "A@" or "AZ"subtract 1 before % 26 and / 26
Trusting decoded input"IIII", "VX", "IC" acceptedthe round-trip check
toHexString for negative numbers-255 prints as ffffff01Integer.toString(n, 16) for a signed value

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. In MCMLXXIV, how does the decoder know to subtract the first C?

Show answer

C (100) is smaller than the next symbol, M (1000), so it's subtracted: M + (M - C) + L + X + X + (V - I) = 1000 + 900 + 50 + 10 + 10 + 4 = 1974. Every symbol only compares itself with its right-hand neighbor.

2. Why does the encoding table include CM, CD, XC, XL, IX and IV?

Show answer

So the greedy loop never has to special-case subtraction: 900 is simply the next-largest value after 1000. Without them, greedy produces non-canonical numerals like DCCCC for 900 and CCCCXXXXIIII for 444.

3. Why does the column-title encoder decrement n before n % 26?

Show answer

Columns use digits 1 to 26 with no zero. Shifting to 0..25 makes the remainder map directly to A..Z and makes the division carry correctly: 26 -> 25 gives "Z" and then 0 stops the loop, instead of a remainder of 0 with no letter.

Key takeaways

  • Decode with one look ahead: subtract a symbol smaller than its right neighbor, and bounds-check the last one.
  • Encode greedily, largest first, with the subtractive pairs as table entries.
  • Greedy is only correct for canonical tables (1, 5, 10, 25 yes; 1, 3, 4 no).
  • Spreadsheet columns are base 26 with no zero: n-- before % 26 and / 26.
  • Decoders accept invalid input; validate by round-tripping.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Number to Romangreedy, 13-entry table
Roman to numberlookahead subtraction
Validatedecode, re-encode, compare
Columnsbijective base 26 (subtract 1)
Signed vs unsigned hextoString(n, 16) vs toHexString
DurationsISO-8601

Snippets worth knowing by heart

Java

StringBuilder sb = new StringBuilder();
while (n > 0) {
    n--;                                              // no zero digit
    sb.append((char) ('A' + n % 26));
    n /= 26;
}
String title = sb.reverse().toString();

Python

def is_valid_roman(s):
    return 1 <= decode(s) <= 3999 and encode(decode(s)) == s

The traps

  • Missing subtractive pairs: 444 becomes CCCCXXXXIIII. Include CM, CD, XC, XL, IX and IV in the table.
  • Lookahead past the end: reading the next symbol overruns the string. The last symbol has no lookahead.
  • A zero digit that doesn't exist: column 26 becomes "A@". Subtract 1 before % 26 and / 26.
  • Trusting decoded input: "IIII", "VX" and "IC" decode happily. Re-encode and compare.
  • toHexString for signed values: -255 prints as ffffff01. Use Integer.toString(n, 16).

Staff-level points, one line each

  • 1..3999 round-trip; the longest numeral is MMMDCCCLXXXVIII.
  • Of 2,800 strings of 1-4 Roman letters, 2,799 decode and only 346 are valid.
  • Columns 1..1,000,000 round-trip, and XFD = 16,384.
  • toString(-255, 16) is "-ff" and toHexString(-255) is "ffffff01".
  • Base -2: 6 is 11010 and -6 is 1110. Every value in -1000..1000 round-trips.

Say it in an interview

  • "Encoding is greedy over a table that includes the subtractive pairs."
  • "Decoding subtracts a symbol smaller than the next one."
  • "Columns have no zero, so I subtract 1 before each digit."
  • "To validate, I decode, re-encode and compare."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 3 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice String Symbol Mapping free

Integer to Roman

MediumProCoding problemJava · Python

Given an integer num (1 to 3999), convert it to a Roman numeral.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice String Symbol Mapping free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime