5
\$\begingroup\$

Problem Definition

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

$$\begin{array}{c|ccccccc|} Symbol & Value \\ \hline I & 1 \\ V & 5 \\ X & 10 \\ L & 50 \\ C & 100 \\ D & 500 \\ M & 1000 \\ \end{array}$$

For example, two is written as II in Roman numeral, just two one's added together. Twelve is written as, XII, which is simply X + II. The number twenty seven is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

I can be placed before V (5) and X (10) to make 4 and 9. X can be placed before L (50) and C (100) to make 40 and 90. C can be placed before D (500) and M (1000) to make 400 and 900. Given a roman numeral, convert it to an integer. Input is guaranteed to be within the range from 1 to 3999.

Examples:

Input: "III" Output: 3

Input: "IV" Output: 4

Input: "IX" Output: 9

Input: "LVIII" Output: 58 (L = 50, V = 5, III = 3).

Input: "MCMXCIV" Output: 1994 (M = 1000, CM = 900, XC = 90 and IV = 4).

Solution

Approach: In my solution I decided to use a simple state machine which seems elegant to me. But do you agree? Could I do better?

Criteria: Readability over performance. I care the most about the code to be easy to understand. Performance is important, but only in terms of "big-O" notation.

Note: . "next symbol" represents "any other case" or "otherwise" branch.

type Action = { value: number, skipNextSymbol: boolean };
type Transitions = { [nextSymbol: string]: Action };
type StateMachine = { [currentSymbol: string]: Transitions };

export const stateMachine: StateMachine = {
  'I': {
    'V': { value: 4, skipNextSymbol: true },
    'X': { value: 9, skipNextSymbol: true },
    '.': { value: 1, skipNextSymbol: false },
  },
  'V': {
    '.': { value: 5, skipNextSymbol: false },
  },
  'X': {
    'L': { value: 40, skipNextSymbol: true },
    'C': { value: 90, skipNextSymbol: true },
    '.': { value: 10, skipNextSymbol: false },
  },
  'L': {
    '.': { value: 50, skipNextSymbol: false },
  },
  'C': {
    'D': { value: 400, skipNextSymbol: true },
    'M': { value: 900, skipNextSymbol: true },
    '.': { value: 100, skipNextSymbol: false },
  },
  'D': {
    '.': { value: 500, skipNextSymbol: false },
  },
  'M': {
    '.': { value: 1000, skipNextSymbol: false },
  },
};

export const romanToInt = function(roman: string): number {
  if (!roman || !roman.length) throw new Error(`${roman} is not a Roman number.`);

  const symbolQueue = roman.toUpperCase().split('');
  let resultNumber = 0;
  while (symbolQueue.length) {
    const currentSymbol = symbolQueue.shift()!;
    const transitions = stateMachine[currentSymbol];
    const nextSymbol = symbolQueue[0];
    const applicableTransition = transitions[nextSymbol] || transitions['.'];
    resultNumber += applicableTransition.value;
    if (applicableTransition.skipNextSymbol && symbolQueue.length)
      symbolQueue.shift();
  }
  return resultNumber;
};

Tests

import { romanToInt } from '../src/leet-code/1-easy/13-roman-to-integer';

describe(romanToInt.name, () => {
  it('Should work', () => {
    interface TestCase {
      input: string;
      expectedOutput: number;
    };

    const testCases: TestCase[] = [
      { input: 'I', expectedOutput: 1 },
      { input: 'II', expectedOutput: 2 },
      { input: 'III', expectedOutput: 3 },
      { input: 'IV', expectedOutput: 4 },
      { input: 'V', expectedOutput: 5 },
      { input: 'VI', expectedOutput: 6 },
      { input: 'VII', expectedOutput: 7 },
      { input: 'VIII', expectedOutput: 8 },
      { input: 'IX', expectedOutput: 9 },
      { input: 'X', expectedOutput: 10 },
      { input: 'XI', expectedOutput: 11 },
      { input: 'XII', expectedOutput: 12 },
      { input: 'XIII', expectedOutput: 13 },
      { input: 'XIV', expectedOutput: 14 },
      { input: 'XV', expectedOutput: 15 },
      { input: 'XVI', expectedOutput: 16 },
      { input: 'XVII', expectedOutput: 17 },
      { input: 'XVIII', expectedOutput: 18 },
      { input: 'XIX', expectedOutput: 19 },
      { input: 'XX', expectedOutput: 20 },
      { input: 'XXX', expectedOutput: 30 },
      { input: 'XL', expectedOutput: 40 },
      { input: 'L', expectedOutput: 50 },
      { input: 'LX', expectedOutput: 60 },
      { input: 'LXX', expectedOutput: 70 },
      { input: 'LXXX', expectedOutput: 80 },
      { input: 'XC', expectedOutput: 90 },
      { input: 'C', expectedOutput: 100 },
      { input: 'CC', expectedOutput: 200 },
      { input: 'CCC', expectedOutput: 300 },
      { input: 'CD', expectedOutput: 400 },
      { input: 'D', expectedOutput: 500 },
      { input: 'DC', expectedOutput: 600 },
      { input: 'DCC', expectedOutput: 700 },
      { input: 'DCCC', expectedOutput: 800 },
      { input: 'CM', expectedOutput: 900 },
      { input: 'M', expectedOutput: 1000 },
      { input: 'MM', expectedOutput: 2000 },
      { input: 'MMM', expectedOutput: 3000 },
      { input: 'MMMM', expectedOutput: 4000 },
      { input: 'MMMMM', expectedOutput: 5000 },
      { input: 'MCMXCIX', expectedOutput: 1999 },
    ];
    testCases.forEach(testCase => {
      const { input, expectedOutput } = testCase;
      expect(romanToInt(input)).toEqual(expectedOutput);
    });
  });
});
```
\$\endgroup\$
5
\$\begingroup\$

You don't need to define each individual transition state since there's a simple general rule: total all the numerals, but subtract the ones that occur in front of a larger numeral.

function romanToInt(roman: string): number {
    const value: {[numeral: string]: number} = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }
    let prev = 0;
    let total = 0;
    for (const numeral of roman.split('').reverse()) {
        const current = value[numeral];  // this will throw on an invalid numeral
        if (current >= prev)
            total += current;
        else 
            total -= current;
        prev = current;
    }
    return total;
}
\$\endgroup\$
  • \$\begingroup\$ Good observation. I can see how your code will allow invalid inputs (i.e. IVX). My code suffers from the same issue. But at least your code is more compact... If I was to make my code work correctly against the invalid inputs (which is throw an error), I'd actually need to define more transitions -- allowed only. Thanks for the review Sam. Upvoted \$\endgroup\$ – Igor Soloydenko Nov 19 '19 at 7:46
  • \$\begingroup\$ If you wanted to catch more invalid numbers, you could add more rules, like: when you subtract a lesser number from a greater number it has to be at least 5x smaller (i.e. no "VX"). Careful making it too strict, IIRC the Romans themselves would sometimes do slightly irregular things. :) \$\endgroup\$ – Sam Stafford Nov 19 '19 at 15:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.