JS :: Summing numbers over a range of lines (with Array.reduce)

If we want to sum numeric values over a range of lines or drafts, we can write a reusable sum function in terms of Array.reduce:

// sum :: [Num] -> Num
const sum = xs =>
    xs.reduce((a, x) => a + x, 0);

Starting with 0 as an accumulator seed value, it adds each current item x to the accumulator a, and returns the sum of the whole list.

So, if we have mapped each line in our draft to a corresponding number,

(for example the number of minutes expressed by a string like 2h 30m at the end of the line)

draft.content
.split('\n')
.map(minsFromHMstring)

where minsFromHMstring might be something like:

// minsFromHMstring :: String -> Int
const minsFromHMstring = s => {
    const m = /\s*(\d+)h\s+(\d+)m\s*$/.exec(s);
    return Boolean(m) ? (
        (60 * parseInt(m[1], 10)) + parseInt(m[2], 10)
    ) : 0;
};

We can just apply sum to this list of numbers:

(() => {

    // main :: () -> IO Int
    const main = () => {

        const intMins = sum(
            draft.content
            .split('\n')
            .map(minsFromHMstring)
        );

        return (
            alert(intMins.toString()),
            intMins
        );
    };

    // sum :: [Num] -> Num
    const sum = xs =>
        xs.reduce((a, x) => a + x, 0);

    // minsFromHMstring :: String -> Int
    const minsFromHMstring = s => {
        const m = /\s*(\d+)h\s+(\d+)m\s*$/.exec(s);
        return Boolean(m) ? (
            (60 * parseInt(m[1], 10)) + parseInt(m[2], 10)
        ) : 0;
    };

    return main();
})();
1 Like

3 Likes

I find these interesting, but there I times I think I’ll just stick with:

let sum = 0;
for (var s in draft.content.split('\n')) {
  const m = /\s*(\d+)h\s+(\d+)m\s*$/.exec(s);
  sum += m ? (60 * parseInt(m[1], 10)) + parseInt(m[2], 10) : 0;
}

Sure – each approach is optimised for different things.

What I personally like about reduce is that it’s

  • pre-built and pre tested
  • takes care of the mechanics for us (no need to set up a loop), and
  • returns a value without the need to depend on mutation and operators like += which need careful handling, and can often trip us up.

The advantage of just pasting and composing reusable functions like

const sum = xs => xs.reduce((a, x) => a + x, 0);

Is, of course, that it spares us some of the tedium of endlessly reinventing (and re-testing) all our wheels : - )

1 Like

PS another balance is, of course, between:

  • economy of ink (brevity of expressions)
  • economy of scripter time ( less puzzled time later, tracking down a bug )

In that light, my preference is to waste less debugging time, and be more relaxed about using more ink.

For example:

Bracketing ternary expressions

Where you have the perfectly clear and functional

sum += m ? (60 * parseInt(m[1], 10)) + parseInt(m[2], 10) : 0

I would tend to add parentheses, just because I have often found bugs that turned on what was (or wasn’t) included in the initial test section, for example.

sum += (m ? (60 * parseInt(m[1], 10)) + parseInt(m[2], 10) : 0)

Explicit or implied coercion

The advantage of implied coercion is that we have to type less. For example your implied coercion of a Regex match (null | Array) to a Bool:

m ?

Or the common coercion of numbers to strings.

Making coercions explicit, eg

Boolean(m) ? 

or

intMins.toString()

Is technically redundant in many contexts, does waste ink, and looks more noisy, but it’s also a way of reducing time looking for bugs later on.

Undetected automatic type-coercions are quite a productive source of unexpected JS bugs. : -)

That’s why for example, I prefer to never use the slower == (equality test after automatic type conversion) and always use the faster === (direct equality test, without type conversion)

2 Likes