DEV Community

Oinak
Oinak

Posted on

Understanding js reduce with Roman Numerals

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>
Enter fullscreen mode Exit fullscreen mode

It is unsurprisingly not very spectacular:
Coversor GUI

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
Enter fullscreen mode Exit fullscreen mode

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;
    })
  }
Enter fullscreen mode Exit fullscreen mode

So far we have seen IIFEs and jQuery-avoidance in action, so you shall be asking: where are my reduces?

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
})({})
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (6)

Collapse
 
insign profile image
Hélio oliveira

This supposed to be easy? Anyway thanks for your time and effort.

Collapse
 
oinak profile image
Oinak

Hi Hélio, thanks for the feedback

I had not yet qualified its difficulty thing, if it appears as 'easy' it is probably a default value when you don't say otherwise (what number would you suggest on the dev.to scale?).

In this case I would love to know what parts you find less accessible and try to go over them and improve/clarify on this post or on another.

This is a format experiment that benefits/assumes the reading of other materials. It is sure more daunting, but I hoped that it allows to combine several concepts together and see the interactions.

Did you read the linked materials?

  • If yes: were they unhelpful?
  • If not: what deterred you from it? Is it discouraging to refer to other sources?

Cheers!

Collapse
 
insign profile image
Hélio oliveira

I give a overall look, and by the title, the content made confuse. Then I started reading and probably found something long and confuse to me. But as I said, I really appreciate your effort and hope this article help others.

Thread Thread
 
oinak profile image
Oinak

Yep, probably the title is misleading, because the content covers more concepts than just reduce and roman numerals, but I am guilty of trying to have a catchy headline, and also afraid that a thoroughly descriptive title could sound boring.

I am sorry it did not serve you well and grateful for your kind comments despite your disappointment.

Thread Thread
 
dannyengelman profile image
Danny Engelman

It can be made easier to read:

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
Enter fullscreen mode Exit fullscreen mode


`

Collapse
 
oinak profile image
Oinak

To learn just how reduce works with less contrived examples, check this other post from @worsnupd