DEV Community

Cover image for Public Solving: Converting Roman numerals to Arabic
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

Public Solving: Converting Roman numerals to Arabic

Today the elves asked us to help with a Roman numeral converter in JavaScript.

You can find the complete puzzle here.

You might have seen Roman numerals before. They look like this:

I = 1
IV = 4
V = 5
VIII = 8
XCV = 95
Enter fullscreen mode Exit fullscreen mode

The above is what we must do, convert the Roman numerals to the Arabic numeric version.

Luckily for us, there are some rules to Roman numerals that can help us!

  • They should follow big to small format
  • If a smaller number proceeds, it's a subtraction
  • There are only 7 values

Thinking about the solution

I had quite a hard time thinking about the smallest possible codebase.

At first thought about adding IV as a value option and filtering out negative numbers.
Thinking a bit more and reading the Roman rules on using the letters, we can filter this out quickly!

All we need to do is check which number was preceding. If this number is smaller, it's a negative one!

And that sparked me to write a super simple reduce method that does all the heavy lifting for us.

Let's see how it works.

Building a Roman to Arabic number converter in JavaScript

The first thing I did was add a mapping object.
This object contains all the Roman letters and their representing value.

const chart = {
  M: 1000,
  D: 500,
  C: 100,
  L: 50,
  X: 10,
  V: 5,
  I: 1,
};
Enter fullscreen mode Exit fullscreen mode

The next thing we need to do is convert the input into an array to use JavaScript array methods.

At this time, I also decided to uppercase it since that's what our mapping tables accept.

input.toUpperCase().split('')
Enter fullscreen mode Exit fullscreen mode

Then we want to use the JavaScript reduce() method. This method is excellent for this purpose because it can pass an accumulator (previous value).

return input
    .toUpperCase()
    .split('')
    .reduce(
      (acc, romanLetter) => {
        // Todo
      },
      [0, 0]
    );
Enter fullscreen mode Exit fullscreen mode

Let me describe what's going on here.

We reduce the array we just created, and then we get the following parameters:

  • acc: The accumulator contains the previous value and starts with the default.
  • romanLetter: The current looped element
  • [0, 0]: This is the default value. I'm using an array to keep track of the total sum and the previous single value.

Then we need to retrieve the value of this roman letter.

const arabicValue = chart[romanLetter];
Enter fullscreen mode Exit fullscreen mode

We can then simply return the total value of our number and the current single value like this.

return [acc[0] += arabicValue, arabicValue];
Enter fullscreen mode Exit fullscreen mode

This works great, as long as there is no negative value like IV.

To fix that, we can introduce a negative offset.
We will check if the previous single value is smaller than the current one.
We should subtract 2 * from the previous value if that is true.

We do 2 times the value since we just added it in the previous loop, and it's actually a subtraction of that specific value.

let negativeOffset = 0;
if (acc[1] < arabicValue) {
  negativeOffset = -(acc[1] * 2);
}
Enter fullscreen mode Exit fullscreen mode

And then, we can simply + this negative value to our total value.

return [(acc[0] += arabicValue + negativeOffset), arabicValue];
Enter fullscreen mode Exit fullscreen mode

Now, in the end, we just need to return only the total value, which is array element 0 from our reduce.

export const romanToArabic = (input) => {
  return input
    .toUpperCase()
    .split('')
    .reduce(
      (acc, romanLetter) => {
        let negativeOffset = 0;
        const arabicValue = chart[romanLetter];
        if (acc[1] < arabicValue) {
          negativeOffset = -(acc[1] * 2);
        }
        return [(acc[0] += arabicValue + negativeOffset), arabicValue];
      },
      [0, 0]
    )[0];
};
Enter fullscreen mode Exit fullscreen mode

Now let's try and run the test to see how we did:

All test green ready to go

This was quite a cool one to do, and I'm sure there are 100 and 1 good solutions.
Let me know what you think of this one or do differently.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Top comments (5)

Collapse
 
lexlohr profile image
Alex Lohr • Edited

I don't have much to contribute, except I would probably destructure the accumulator to meaningful names and const the negative offset, e.g.

  .reduce(([result, previous], numeral) => {
    const decimal = chart[numeral]
    const negated = previous < decimal
      ? 2 * previous
      : 0
    return [result + decimal - negated, decimal]
  }, [0, 0])[0]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dailydevtips1 profile image
Chris Bongers

Nice!
Did you like my approach on the negative evaluation and abusing the reduce accumulator for that?

Collapse
 
lexlohr profile image
Alex Lohr

Yes, it's hard to improve. Though I must admit that I used the same approach with the negative evaluation ~30 years ago in school for the same task, though this was written in Pascal, so there was no reduce method.

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

We are so used to reading from Left to Right, we overlook the Right to Left alternatives.

The whole point of Roman notation is you want to check if I becomes before X

That is much easier when you reverse the Roman string.

Or in JavaScript, use the hardly ever used reduceRight method

(code optimized for best GZIP/Brotli compression)

jsfiddle.net/dannye/Lj6o7hsm/

const romanToArabic = (input) => [...input].reduceRight((
  acc,
  letter,
  idx,
  arr,
  value = {m:1000, d:500, c:100, l:50, x:10, v:5, i:1}[letter.toLowerCase()],
  doubleSubtraction = letter == arr[idx + 1] // ignore IIX notation
) => {
  if (value < acc.high && !doubleSubtraction) { 
    acc.Arabic -= value;
  } else {
    acc.high = value;
    acc.Arabic += value;
  }
  console.log(idx, letter, acc, 'value:', value, acc.high, arr[idx + 1]);
  return acc;
}, { high: 0, Arabic: 0 }).Arabic; // return Arabic value

Object.entries({
    "cxxiv": 124,
    "ix": 9,
    "iix": 10,
    "xL": 40,
    "MMMDXLIX": 3549,
    "MMMMCMXCIX": 4999}
  ).map(([roman,value,converted = romanToArabic(roman)])=>{
    console.log(roman, "=", converted);
    console.assert(converted == value, "wrong conversion,", roman, "must be", value)
})
Enter fullscreen mode Exit fullscreen mode


`

Collapse
 
dailydevtips1 profile image
Chris Bongers

Interesting approach Danny!

But I guess in terms of calculation we are kinda doing the same calculation, but in the opposite order.

Nice point on that's how it it should be read based on what comes first.