Problem Definition
Roman numerals are represented by seven different symbols:
I
,V
,X
,L
,C
,D
andM
.
$$\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 simplyX
+II
. The number twenty seven is written asXXVII
, which isXX
+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 asIV
. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written asIX
. There are six instances where subtraction is used:
I
can be placed beforeV
(5
) andX
(10
) to make4
and9
.X
can be placed beforeL
(50
) andC
(100
) to make40
and90
.C
can be placed beforeD
(500
) andM
(1000
) to make400
and900
. Given a roman numeral, convert it to an integer. Input is guaranteed to be within the range from1
to3999
.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
andIV
=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);
});
});
});
```
1 Answer 1
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;
}
-
\$\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– Igor Soloydenko2019年11月19日 07:46:25 +00:00Commented Nov 19, 2019 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\$Samwise– Samwise2019年11月19日 15:31:23 +00:00Commented Nov 19, 2019 at 15:31
Explore related questions
See similar questions with these tags.