Roman Numerals Are a Subtractive Notation: Parsing Them Backwards Is a Bug Factory

Roman numerals look like a simple lookup table. They are not — they are a positional system with a subtractive rule, and almost every "quick converter" gets the parsing direction wrong. If you have ever needed to read a copyright year, an outline numbering scheme, or a regex that matches MCMXCIV, this is the part that matters.

The symbol table and the rules

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

The subtractive rule: a smaller numeral placed before a larger one subtracts, and only these pairs are legal:

  • IV = 4, IX = 9
  • XL = 40, XC = 90
  • CD = 400, CM = 900

The critical structural fact: the symbols appear in strictly non-increasing value order, except for the single subtractive pair. That is what makes a linear scan correct.

The algorithm

Scan left to right. If the current symbol is smaller than the next one, subtract it; otherwise add it.

const VALUE = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; function parseRoman(input) { const s = String(input).toUpperCase().replace(/\s+/g, ''); if (!/^[IVXLCDM]+$/.test(s)) return null; // illegal characters let total = 0; for (let i = 0; i < s.length; i++) { const cur = VALUE[s[i]]; const next = i + 1 < s.length ? VALUE[s[i + 1]] : 0; total += cur < next ? -cur : cur; // reject nonsense the value-only scan accepts if (!isValidForm(s)) return null; return total; } function isValidForm(s) { // no symbol repeated more than 3 times in a row if (/I{4}|X{4}|C{4}|M{4}/.test(s)) return false; // V, L, D are never repeated if (/V{2}|L{2}|D{2}/.test(s)) return false; // subtractive pairs only, and never repeated if (/IL|IC|ID|IM|XD|XM|VX|LC|DM/.test(s)) return false; return true; } 

Worked example

MCMXCIV (the year 1994):

Step Current Next Comparison Running total
1 M = 1000 C = 100 1000 > 100 → add 1000
2 C = 100 M = 1000 100 < 1000 → subtract 900
3 M = 1000 X = 10 1000 > 10 → add 1900
4 X = 10 C = 100 10 < 100 → subtract 1890
5 C = 100 I = 1 100 > 1 → add 1990
6 I = 1 V = 5 1 < 5 → subtract 1989
7 V = 5 — add 1994

The two subtractive pairs CM (900) and IV (4) are handled by the same branch that handles everything else. No special-casing, no lookup of two-character sequences.

Edge cases that actually bite

IIII vs IV. Both scan to 4 if you only sum values. Clock faces use IIII; the subtractive form is IV. Decide which convention you accept and reject the other — silently accepting both means IIII and IV are interchangeable in your data, which breaks sorting and deduplication.

Illegal subtractive pairs. IC (99), IL (49), XM (990), VX (5) are all commonly produced by naive encoders. The value-only scan computes IC = 99 and reports success. Only the isValidForm pass catches it. If you only need the value and not the canonical form, document that you accept non-standard input — otherwise your parser is silently lossy.

Repeated V, L, D. VV scans to 10, which is X. A validator that only checks totals will accept it. These symbols are never repeated in correct notation.

Empty string and whitespace. "" should be invalid, not 0. Trim, then require at least one symbol.

Unicode lookalikes. Ⅰ (U+2160) and I (U+0049) are different code points. If your input comes from OCR or a PDF, normalize or reject — /^[IVXLCDM]+$/ already rejects the Unicode forms, which is the safe default.

Case sensitivity. Accept mcmxciv and normalize with toUpperCase(); rejecting lowercase is a usability bug, not a correctness feature.

Large values. Classical notation stops at 3999 (MMMCMXCIX). Larger values need vinculum (bar) notation, which is a multiplier of 1000. If your users need years beyond 3999, you need a convention, not just a bigger M count.

Common mistakes

  • Adding without comparing to the next symbol. A pure sum of symbol values gives MCMXCIV = 2216, not 1994. The comparison is the whole algorithm.
  • Special-casing two-character pairs with indexOf. It works for valid input and fails on mixed forms. The scan is shorter and more general.
  • Encoding by greedy subtraction only. 49 should be XLIX, not IL. A correct encoder tries the largest symbol including the legal subtractive pairs, in order.
  • Assuming the input is valid. If the string comes from a user or an OCR pipeline, validate. ABCIX should be rejected, not parsed as IX = 9.
  • Using the converter as a sort key without zero-padding. IX (9) sorts before X (10) lexicographically but after it numerically. Store the integer, not the string.

A converter that shows its work

If you want to check a value in both directions — including the canonical-form validation — the converter at https://www.miryori.org/tools/roman-numerals-converter does the scan and rejects non-standard forms like IC and IIII rather than silently accepting them.

Note: numeral conventions vary by era and by clock-face tradition; the tool follows classical subtractive notation.