Understanding js reduce with Roman Numerals
Oinak
Posted on September 9, 2019
There are several ways to fix new concepts in your head, to use them, to repeat, to combine with new circumstances...
To do so, we will build a Roman numerals to Arabic conversion form, and the corresponding Arabic numerals to roman.
I will take learnings from previous posts and other sources to try to solidify the use of one of my favourite ruby constructs: reduce
(a.k.a: inject
), but in its javascript version.
Here are our three sources:
I)
I will take this post from @sandimetz as a starting point. Please take a moment to read it so you can follow along.
II)
The IIFE's from my own post, to separate conversion logic from interface/behaviour.
III)
A very minimalistic interface by using what we saw on this other post of mine about omiting jQuery.
I suggest reading them beforehand, but you might prefer to wait until you feel the need of them as you may already now what is explained in any or all of them.
So, HTML for this is absolutely minimal. I am not going to do steps, bear with me. There are two inputs, identified as roman and arabic. Each one of the inputs has an accompanying span, called respectively: toArabic and toRoman.
We load to mysterious files numeral.js
and conversor.js
, and then an inline script that invokes something called Oinak.Coversor.init
and passes the id's of the inputs and spans to it.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Roman Numerals</title>
</head>
<body>
<div>
Roman:<input name="roman" id="roman"> = <span id="toArabic">
</div>
<div>
Arabic: <input name="arabic" id="arabic"> = <span id="toRoman">
</div>
<script src="numeral.js"></script>
<script src="conversor.js"></script>
<script>
Oinak.Conversor.init({
arabic: '#arabic',
toArabic: '#toArabic',
roman: '#roman',
toRoman: '#toRoman',
})
</script>
</body>
</html>
It is unsurprisingly not very spectacular:
The idea is that as you write roman numbers (I, IX, CDVII...) on the roman
input, arabic digits appear on the toArabic
span. On the same spirit, if you input arabic numbers (1, 9, 407...) on the arabic
input, the toRoman
span updates with the conversion.
There is no error control, for brevity, but you might want yo add it yourself at the end as an extra-credit exercise :).
IIFE's and not-jQuery
In the conversor.js
we have an IIFE like those we talked about on the aforementioned post.
Let's see if from the outside-in:
// namespace
window.Oinak = window.Oinak || {}
window.Oinak.Conversor = ((expose) => {
// private vars
let roman, arabic, toRoman, toArabic;
// auxiliar methods
observeRoman = () => {...}
observeArabic = () => {...}
//public interface
expose.init = (options) => {
roman = options.roman;
arabic = options.arabic;
toRoman = options.toRoman;
toArabic = options.toArabic;
observeRoman();
observeArabic();
}
return expose;
})({}) // immediate invocation
If you ignore the auxiliar methods, this is just copy & paste & rename from the IIFE's post.
Now, the auxiliar functions are the ones that connect this with the other file. They are almost identical so I will comment (inline) just the first one:
observeRoman = () => {
// get the elements as we learnt on the no-jQuery post:
let source = document.querySelector(roman); // arabic on the other
let target = document.querySelector(toArabic); // toRoman on the other
// observe the event natively:
source.addEventListener('input', e => {
let from = e.target.value;
// call to "something" magic
let to = Oinak.Numeral.r_to_i(from); // use i_to_r for reverse converison
// ...and show the result on the span
target.innerText = to;
})
}
So far we have seen IIFEs and jQuery-avoidance in action, so you shall be asking: where are my reduce
s?
Reduce like there is no tomorrow:
So, first of all, what is reduce
?
As a simplification, is a function that
- takes an initial value
- stores it on an accumulator
- iterates over a list (or object, or iterable...) and
- for each item in the list, performs a custom operation(between accumulator and item)
- stores the result as the new value for accumulator
- and finally returns the last value of the accumulator
function reduce(combine, initialValue){
let accumulator = initialValue;
for (let item in list) {
accumulator = combine(accumulator, item);
}
return accumulator;
}
This pattern is so common, that most modern languages provide it.
Javascript Array does too now.
But, as it requires you to hold both the concept of reduce
itself, and the indirection of a callback, it can be daunting for some people.
In this example, I have purposely avoided the use of anonymous callbacks for reduce
to try to make it more legible.
I am omitting the explanation from the conversion logic because that's what Sandi's post is about and I am not going to explain anything better than @sandimetz ever, no matter how early I get up in the morning.
Look at these examples of reduce
, specially the one in to_roman
which is using a complex accumulator to be able to use and modify a second external value from within the callback, without strange hoisting stuff.
I kept accumulator
and reducer
names fixed so it is easier for you to refer to the documentation (linked before) and analyse what each of them is doing.
So, without further ceremony:
window.Oinak = window.Oinak || {}
window.Oinak.Numeral = ((expose) => {
const ROMAN_NUMERALS = {
1000: 'M', 500: 'D', 100: 'C', 50: 'L', 10: 'X', 5: 'V', 1: 'I'
};
const LONG_TO_SHORT_MAP = {
'DCCCC': 'CM', // 900
'CCCC': 'CD', // 400
'LXXXX': 'XC', // 90
'XXXX': 'XL', // 40
'VIIII': 'IX', // 9
'IIII': 'IV' // 4
};
to_roman = (number) => {
const reducer = (accumulator, [value, letter]) => {
let times = Math.floor(accumulator.remaining / value);
let rest = accumulator.remaining % value;
accumulator.remaining = rest;
accumulator.output += letter.repeat(times); // 30/10 = 'X' 3 times
return accumulator;
}
let initialValue = { remaining: number, output: '' };
let list = Object.entries(ROMAN_NUMERALS).reverse(); // bigger nums first
let reduction = list.reduce(reducer, initialValue);
return reduction.output;
};
to_number = (roman) => {
let additive = to_additive(roman);
reducer = (total, letter) => total + parseInt(invert(ROMAN_NUMERALS)[letter]);
return additive.split('').reduce(reducer, 0);
}
convert = (map, string) => {
const reducer = (accumulator, [group, replacement]) => {
return accumulator.replace(group, replacement)
}
return Object.entries(map).reduce(reducer, string);
}
// return a new object with values of the original as keys, and keys as values
invert = (obj) => {
var new_obj = {};
for (var prop in obj) {
if(obj.hasOwnProperty(prop)) {
new_obj[obj[prop]] = prop;
}
}
return new_obj;
};
// to understand the two step conversion, read Sandi's post!
to_additive = (string) => convert(invert(LONG_TO_SHORT_MAP), string)
to_substractive = (string) => convert(LONG_TO_SHORT_MAP, string)
expose.i_to_r = (number) => to_substractive(to_roman(number))
expose.r_to_i = (string) => to_number(to_additive(string))
return expose;
})({})
That's it, with that you have a Roman to Arabic and an Arabic to Roman number conversion.
I hope you like it. If you want to play with it you can find it here.
Were you using reduce
already? If that's the case, do you have other interesting examples. If not, do you feel better prepared to use it now?
Tell me in the comments!
Posted on September 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.