DEV Community

Tyler Scott Williams
Tyler Scott Williams

Posted on • Edited on • Originally published at ogdenstudios.xyz

Convert Roman Numerals to Integers

This post is part of my "LeetCode for 1x Developers" series, in which I struggle through LeetCode problems. Sometimes I figure it out, other times I don't. Either way, I give each problem my best shot, and write up my thought process through the challenges

Problem description

Problem on leetcode

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

  • I: 1
  • V: 5
  • X: 10
  • L: 50
  • C: 100
  • D: 500
  • M: 1000

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.

Intuition

This problem feels like brute-force by its nature. There are a lot of rules and what feel like inconsistent patterns on the surface. So to start, I really just wanted to codify all these edge cases and different values.

At the end of function, I need to return some numeric value. So I know I'll start at 0, and all of the numerals are additive, so I'll be adding to it.

But the trick is that there are these combinations of prefixed numerals that generate distinct values. So I wrote up a pretty length if/else branch logic. It takes the input string, checks the first character against any of the possible prefixes. If that character is sometimes found in front of other characters, we check the next one to see the possible value. We set a length variable to 2 to indicate that this particular instance is a two-character value.

If there is no second character, we set length to 1.

We add to the result value, based on the values of each numeral.

Finally, we subtract either 1 or 2 numerals from the front of the string and repeat this process until the input has length 0.

Here's my first pass. It's a little ugly, and I kept missing values from the prompt, so I added them in poor order:

var romanToInt = function(s) {
    let result = 0;

    while(s.length > 0) {
        let length; 
        if (s.charAt(0) === 'I') {
            if (s.charAt(1) === 'V') {
                result += 4;
                length = 2;
            } else if (s.charAt(1) === 'X') {
                result += 9;
                length = 2;
            } else {
                result += 1;
                length = 1;
            } 
        } else if (s.charAt(0) === 'X') {
            if (s.charAt(1) === 'L') {
                result += 40
                length = 2;
            } else if (s.charAt(1) === 'C') {
                result += 90;
                length = 2;
            } else {
                result += 10;
                length = 1;
            }
        } else if (s.charAt(0) === 'C') {
            if (s.charAt(1) === 'D') {
                result += 400;
                length = 2;
            } else if (s.charAt(1) === 'M') {
                result += 900;
                length = 2;
            } else {
                result += 100;
                length = 1;
            }
        } else if (s.charAt(0) === 'V') {
            result += 5; 
            length = 1;
        } else if (s.charAt(0) === 'L') {
            result += 50;
            length = 1;
        } else if (s.charAt(0) === 'D') {
            result += 500;
            length = 1;
        } else if (s.charAt(0) === 'M') {
            result += 1000;
            length = 1;
        }
        s = s.substr(length);
    }
    return result;
};
Enter fullscreen mode Exit fullscreen mode

This can be much... much cleaner. I found a great solution in the discussion section that looks like this:

Solution

var romanToInt = function(s) {
    var map = {
        'I': 1, 
        'V': 5, 
        'X': 10, 
        'L', 50,
        'C': 100,
        'D': 500, 
        'M': 1000
    }

    var number = 0;
    var index; 

    if (s.indexOf('CM') != -1) number -= 200;
    if (s.indexOf('CD') != -1) number -= 200;
    if (s.indexOf('XC') != -1) number -= 20;
    if (s.indexOf('XL') != -1) number -= 20;
    if (s.indexOf('IX') != -1) number -= 2;
    if (s.indexOf('IV') != -1) number -= 2;

    for (let i=0; i<s.length; i++) {
        number += map[s[i]];
    }

    return number;
}
Enter fullscreen mode Exit fullscreen mode

This solution is super clean and I like it a lot. It sets up a map object of all the numerals and their values. Then it initializes the return value at 0.

Next, it checks for the edge cases: CM, CD, XC, XL, IX, and IV. If the input string contains any of these, it subtracts from the initial value.

Then it runs a for loop against the input string and adds the value from the map of each character. Since we checked for the prefixed edge cases and subtracted the appropriate values, the final result is correct, even with the edge cases.

It took me a minute to visualize the values here. So here's an example. Consider an input: "XIV".

Without the prefix checks, the for loop would return 16. But since the string has an indexOf not equal to -1 for IV, we subtract 2 from the initial value. This means the naive for loop returns 14, the correct answer.

It's a neat approach. I like it more than my big long branch. It's well organized, although I do think there's a bit of counter-intuitive logic happening with the initial subtraction of the prefix values.

Top comments (3)

Collapse
 
habilain profile image
David Griffin • Edited

Special casing all those checks is asking for problems though, and I'm pretty sure it's not that neat. If we assume that the input is a valid Roman numeral (which this code does do; "IIX" is not a valid Roman numeral, but your code will parse it as 10), then the rule is that "if a numeral is smaller than the next numeral, subtract its value". Using that rule gives a much neater function:

vals = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}

def roman_to_int(input_string: str) -> int:
    ints = [vals[x] for x in input_string] + [0]
    output = 0
    for current, next in zip(ints, ints[1:]):
        output += -current if current < next else current
    return output

(Apologies for using Python, but I don't trust my JS enough to write it error free) (Edited to add the vals dict so the code actually runs)

Collapse
 
ogdenstudios profile image
Tyler Scott Williams

Ah yeah! Good catch. I think that's a given in the LC problem but I forgot to say it. If it isn't, none of the LC cases provide invalid numerals, haha

Collapse
 
pozil profile image
Philippe Ozil • Edited

Interesting problem, thanks for sharing.

@everyone who might read and for the sake of not re-inventing the wheel, note that there's a CSS rule for list style:

ol { list-style-type: upper-roman; }