Things are starting to get really interesting, and will probably end up a little long, this next bonfire will have us write a roman numeral converter. The functionality is quite clear, take in a number and output a roman numeral of equivalent value.
You may, or may not be familiar with roman numerals. For completions sake, I’ll give a quick explanation and link you to the same source Free Code Camp does, so you can get some further insight. In short, roman numerals are symbols that represent numbers, as used by ancient Romans. The basic units are shown below:
- 1 – I
- 5 – V
- 10 – X
- 50 – L
- 100 – C
- 500 – D
- 1000 – M
These can be combined to get pretty much any number. There is a few rules you must abide to while combining, when a symbol appears after a larger symbol, then, it is added. When used before, it is substracted. You also can’t use the same symbol more than 3 times in a row. Here’s a few examples:
- 1 – I
- 2 – II
- 3 – III
- 4 – IV
- 5 – V
- 6 – VI
- 7 – VII
- 8 – VIII
- 9 – IX
- 10 – X
- 11 – XI
- 12 – XII
- 30 – XXX
- 40 – XL
- 50 – L
For larger numbers, break the numbers like shown below:
1991
1000 – M
900 – CM
90 – XC
1 – I
————–
MCMXCI
This is key when trying to figure out a way of automating this kind of logic and implementing it in a function. If you want some additional info on the usbject, take a look at this site.
Let’s get coding! As always, there is a few ways to approach this problem. First, I’ll show you the tedious, long way that does the job.
For this function, first, I’ll turn our number into an array, similar to how we converted the number 1991 (1000, 900, 90 and 1). We’ll place this logic into a function. This way, we simplify the whole process.
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list; } }
Once we have our list in place, we must iterate over every element, convert them to a roman (using another function) number and concatenate the result:
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list; } function romanize(num) { // Do some magic. } var total = ''; num = numList(num); for (var i = num.length - 1; i >= 0; i--) { total += romanize(num[i]); } return total; }
Now, we just need the romanize function to actually work. It may seem simple at first, but it’s got some intricacies that we must have in mind. First of all, remember that sometimes we place the letter on the left of the greater symbol (to substract), but this is not the case every time. What we do know, is that everytime we get a number that has an exact roman numeral counterpart, that symbol can be returned inmediatly without any kind of calculation.
Let’s start by creating two arrays that contain the roman symbols and number counterparts:
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list; } function romanize(num) { var roman = ['I', 'V', 'X', 'L', 'C', 'D', 'M']; var numbers = [1, 5, 10, 50, 100, 500, 1000]; if (numbers.indexOf(num) >= 0) { // Checks for an exact match return roman[numbers.indexOf(num)]; // and returns roman numeral. }; // Do some magic. } var total = ''; num = numList(num); for (var i = num.length - 1; i >= 0; i--) { total += romanize(num[i]); } return total; }
If we did not find an exact match, we need a way to check if the number needs substraction (such as 9 – IX) or we can add on top of it (such as 12 – XII).
To get this right, we’ll loop over the numbers array, and select the closest to our number (a larger number if we need to substract and a smaller one if we need to add). We know that we cannot place three equal symbols in a row, so we can assume the following:
if ((closestNumberBelow * 3 === myNumber || closestNumberBelow + secondClosestNumberBelow * 3 >= myNumber) || myNumber <= 3) { // Number needs adding. // We check for <= 3 because they will always need adding. } else { // Number needs substraction. }
Let’s implement this logic and get a variable containing the closest numbers’ index going:
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list; } function romanize(num) { var roman = ['I', 'V', 'X', 'L', 'C', 'D', 'M']; var numbers = [1, 5, 10, 50, 100, 500, 1000]; if (numbers.indexOf(num) >= 0) { return roman[numbers.indexOf(num)]; }; var closest; var result = ''; for (var i = 0; i < numbers.length; i++) { if (num < numbers[i]) { closest = i - 1; if ((numbers[closest] * 3 === num || numbers[closest] + numbers[closest - 1] * 3 >= num) || num <= 3) { // Add! } else { // Substract! } break; } } return result; } var total = ''; num = numList(num); for (var i = num.length - 1; i >= 0; i--) { total += romanize(num[i]); } return total; }
As you can see, we loop through the elements in the numbers array, and as soon as we get a value larger than the input, we check if we need to add or substract and act acordingly.
Now, we must implement this adding and substracting logic. Adding will place the roman symbol on the right hand side, and substracting will place it on the left. Once we get the symbol, we substract it from the input number in both cases, and proceed to get the roman equivalent of the remainder (using recursion).
When substracting, we will end up with a negative number, so we must get the absolute value of the remainder:
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list; } function romanize(num) { var roman = ['I', 'V', 'X', 'L', 'C', 'D', 'M']; var numbers = [1, 5, 10, 50, 100, 500, 1000]; if (numbers.indexOf(num) >= 0) { return roman[numbers.indexOf(num)]; }; var closest; var result = ''; for (var i = 0; i < numbers.length; i++) { if (num < numbers[i]) { closest = i - 1; if ((numbers[closest] * 3 === num || numbers[closest] + numbers[closest - 1] * 3 >= num) || num <= 3) { num -= numbers[closest]; result = roman[closest] + romanize(num); } else { closest++; num = Math.abs(num - numbers[closest]); result = romanize(num) + roman[closest]; } break; } } return result; } var total = ''; num = numList(num); for (var i = num.length - 1; i >= 0; i--) { total += romanize(num[i]); } return total; }
And voila! Here is a quite inefficient, overly complicated function that many will probably not understand at all. Additionally, it breaks on some use cases, so we have to watch out for that. This next version fixes those problems, see if you can find them!
function convert(num) { function numList(num) { var list = String(num).split('').reverse(); for (var i = list.length - 1; i >= 0; i--) { list[i] = Number(list[i]) * Math.pow(10, i); } return list.filter(function(item) { return item === 0 ? false : true; }); } function romanize(num) { var roman = ['I', 'V', 'X', 'L', 'C', 'D', 'M']; var numbers = [1, 5, 10, 50, 100, 500, 1000]; if (numbers.indexOf(num) >= 0) { return roman[numbers.indexOf(num)]; } else if (num % 1000 === 0) { var amount = num / 1000; var value = ''; for (var i = 1; i <= amount; i++) { value += 'M'; }; return value; }; var closest; var result = ''; for (var i = 0; i < numbers.length; i++) { if (num < numbers[i]) { closest = i - 1; if ((numbers[closest] * 3 === num || numbers[closest] + numbers[closest - 1] * 3 >= num) || num <= 3) { num -= numbers[closest]; result = roman[closest] + romanize(num); } else { closest++; num = Math.abs(num - numbers[closest]); result = romanize(num) + roman[closest]; } break; } } return result; } var total = ''; num = numList(num); for (var i = num.length - 1; i >= 0; i--) { total += romanize(num[i]); } return total; }
Next, we are going to “cheat”. Cheating while programming may actually be a good thing, as long as it works, since we are going to be increasing performance and simplifying out function quite heavily.
The computer is stupid, and that’s why we have to tell it what roman symbols are equivalent to every number, and when to substract or add in a per-case basis. So, why don’t we tell it the whole story, and skip all of this add or substract process? We might need to type a few extra roman numbers, but we only need to do it once!
Here’s another version of the function:
function convert(num) { var romanEquiv = { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 }; var roman = ''; for (var key in romanEquiv) { while (num >= romanEquiv[key]) { roman += key; num -= romanEquiv[key]; } } return roman; }
We just skipped over the whole process of substraction by simply giving the computer all the cases where substraction actually has to occur. We create a dictionary-like object with every roman numeral we need, and then loop through every key and add it to our result, while substracting it from the input number as long as it is larger.