Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ

Today, we’re going time travelling! Let’s go back to the year CCXVII, so 217, all the way to the iron age: The Roman empire.

But today we’re not exploring the Colosseum or the Pantheon, neither will we talk to legionnaires or walk the Cursus publicus….


This content originally appeared on DEV Community and was authored by Pascal Thormeier

Today, we're going time travelling! Let's go back to the year CCXVII, so 217, all the way to the iron age: The Roman empire.

But today we're not exploring the Colosseum or the Pantheon, neither will we talk to legionnaires or walk the Cursus publicus. Instead we'll learn about a concept that enabled a big part of the Roman economy as well as some of the most magnificent architectural masterpieces. Today's topic are Roman numerals.

Wait, how on earth does CCXVII translate to 217?

A very good question! Let's analyse.

(Short interlude: In case you didn't know, the digits we're used to (0-9), are called "Arabic numerals", since they originated in the western part of the Arabian and North African part of the world.)

First of all, what do C, X, V and I mean?

This table gives an overview of the Roman numeral digits and their value:

Roman numeral digit Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

Just like Arabic numerals, Roman numerals consist of digits. The digits, however, don't exactly correspond to different values at different places (for example, 217 would be 2 * 100, 1 * 10 and 7 * 1), but instead, they add to a larger number. The amount of same digits correspond to the value. We could therefore rewrite CCXVII to C + C + X + V + I + I. With the above table, this translates to 100 + 100 + 10 + 5 + 1 + 1 = 217.

So, for example, the number 4 could be written as IIII, right? Almost! While this might be the intuitive answer, the inventors decided that this was not the way to go. Instead, everything that cannot be written with an addition of maximum three same digits is written as a subtraction from the next larger number. So, instead of writing 1 + 1 + 1 + 1 = 4, we write 5 - 1 = 4, in Roman numerals V - I or simply IV.

In summary, this means if digit A (left of digit B) is smaller than digit B, it's subtracted, otherwise added. To illustrate:

IV --> I < V --> V - I
But:
VI --> V > I --> V + I

This works for any number:

CDXLIV 
--> (D - C) + (L - X) + (V - I) 
= (500 - 100) + (50 - 10) + (5 - 1) = 444

XC = (100 - 10) = 90

However, 99 is not written as 100 - 1, but as (100 - 10) + (10 - 1).

In summary, these are the rules to convert a single digit base 10 number N to Roman numerals:

  • If N <= 3, repeat I 1 to 3 times
  • If N === 4, it's 5 - 1, so VI
  • If N === 5, it's V
  • If N < 9, it's 5 + repeat I 1 to 3 times
  • If N === 9, it's 10 - 1, so IX

If we look at the above table, we'll notice that for every power of 10 up to 1000 (1, 10, 100, 1000), there's singles (1, 10, etc.) and fives (5, 50, 500) - we can therefore repeat the steps above for every power of 10 and change the set of digits we use accordingly.

Coding from base10 to Roman

First, we translate the usual base 10 numbers into Roman numerals.

We need a simple map of Roman numerals to numbers:

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

Next, we need to implement the rules for converting single digits. The rules above can be translated to a set of if statements directly, we only need to know the power of 10 as well, so we chose the right Roman numeral digits:

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

/**
 * Translates a single digit in respect of the power of 10 into a Roman numeral.
 * @param n
 * @param powerOf10
 * @returns {*|string}
 */
const numDigitToRomDigits = (n, powerOf10) => {
  if (n <= 3) { // I, II, III, X, X, XXX, C, CC, CCC
    return romanNumerals[powerOf10].repeat(n)
  }

  if (n === 4) { // IV, XL, CD
    return romanNumerals[powerOf10] 
      + romanNumerals[powerOf10 * 5]
  }

  if (n === 5) { // V, L, D
    return romanNumerals[powerOf10 * 5]
  }

  if (n < 9) { // VI, VII, VIII, etc.
    return romanNumerals[powerOf10 * 5] 
      + romanNumerals[powerOf10].repeat(n - 5)
  }

  // MC, XC, IX
  return romanNumerals[powerOf10] 
    + romanNumerals[powerOf10 * 10]
}

Let's try this out:

numDigitToRomDigits(7, 10) // "70", yields `LXX`
numDigitToRomDigits(5, 100) // "500", yields `D`
numDigitToRomDigits(3, 1) // "3", yields `III`
numDigitToRomDigits(4, 10) // "40", yields `XL`

That looks good! Now, we can use this function to convert larger numbers:

/**
 * Translates an entire number to Roman numerals.
 * @param x
 * @returns {string}
 */
const num2rom = x => {
  // Split number into digits and reverse, 
  // so figuring out the power of 10 is easier.
  const digits = x.toString()
    .split('')
    .map(n => parseInt(n))
    .reverse()

  // Larger numbers don't work, 5000 is written 
  // as V with a dash on top, we don't have that 
  // character...
  if (x > 3999) {
    throw new Error(
      'Numbers larger than 3999 cannot be converted'
    )
  }

  // Loop over all digits, convert them each
  let romanNum = ''
  for (let i = 0; i < digits.length; i++) {
    romanNum = 
      numDigitToRomDigits(digits[i], 10 ** i) 
      + romanNum // Attach to front of already converted
  }

  return romanNum
}

Let's try that:

num2rom(3724) // yields `MMMDCCXXIV` - works!

From Roman numerals to base 10 again

The other way is going to be bit trickier - we need to parse Roman numerals and convert them back to base10 again. First, we flip the map from earlier. Stackoverflow tells us how to do this.

const flipObject = obj => Object.entries(obj)
  .reduce((acc, [key, value]) => (acc[value] = key, acc), {})

const base10Numerals = flipObject(romanNumerals)

/* yields
{
  C: "100"
  D: "500"
  I: "1"
  L: "50"
  M: "1000"
  V: "5"
  X: "10"
}
*/

The subtraction/addition method is what we're going to implement now. We know that larger numbers left of other numbers are added. If the number on the left is smaller, it's subtracted. So, basically: VI = V + I, but IV = V - I. Since there's no such thing as IIV, we can check the number that comes next to determine if we add or subtract the current number. So, something like this:

From left to right,
If next number to the right is larger:
  Subtract current digit
Else
  Add current digit

In code, it would look like this:

/**
 * Converts a roman number to base10.
 * @param x
 * @returns {number}
 */
const rom2num = x => {
  // Split number and assign base10 
  // value to each digit.
  // parseInt is necessary, because the 
  // flip yields strings.
  const digits = x.split('')
    .map(d => parseInt(base10Numerals[d]))

  let sum = 0
  // Loop over every digit
  for (let i = 0; i < digits.length; i++) {
    // If number to the right larger than the 
    // current number
    if (digits[i + 1] > digits[i]) {
      sum -= digits[i]
    } else {
      sum += digits[i]
    }
  }

  return sum
}

Let's see if that works by converting all numbers from 1 to 3999 back and forth:

let result = true
for (let i = 0; i < 3999; i++) {
  result = result && rom2num(num2rom(i)) === i
}

console.log(result) // true, works!

The result

Now we need some input fields and buttons, and voila:

Phew, enough ancient times for now, let's go back to the 21st century.

I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a โค๏ธ or a ๐Ÿฆ„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee โ˜• or follow me on Twitter ๐Ÿฆ! You can also support me directly via Paypal!

Buy me a coffee button


This content originally appeared on DEV Community and was authored by Pascal Thormeier


Print Share Comment Cite Upload Translate Updates
APA

Pascal Thormeier | Sciencx (2021-10-04T15:05:34+00:00) Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ. Retrieved from https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/

MLA
" » Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ." Pascal Thormeier | Sciencx - Monday October 4, 2021, https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/
HARVARD
Pascal Thormeier | Sciencx Monday October 4, 2021 » Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ., viewed ,<https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/>
VANCOUVER
Pascal Thormeier | Sciencx - » Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/
CHICAGO
" » Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ." Pascal Thormeier | Sciencx - Accessed . https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/
IEEE
" » Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ." Pascal Thormeier | Sciencx [Online]. Available: https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/. [Accessed: ]
rf:citation
» Ancient computer science: Let’s build a Roman numeral converter from scratch ๐Ÿบ๐Ÿ“œ | Pascal Thormeier | Sciencx | https://www.scien.cx/2021/10/04/ancient-computer-science-lets-build-a-roman-numeral-converter-from-scratch-%f0%9f%8f%ba%f0%9f%93%9c/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.