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);
});
});
});
```