Moving Average in Lodash

Eric Normand's Newsletter
Software design, functional programming, and software engineering practices
Over 5,000 subscribers

Let's write a Moving Average function in JavaScript with Lodash.

But first, we'll review the regular average function. How does that look in JavaScript without Lodash?

function average(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) sum += numbers[i];
  return sum / (numbers.length || 1);
}

The only tricky thing is that we can't divide by zero, so we are using the fact that 0 is falsey to default numbers.length to 1. In that case, we'll just return 0. Not technically correct but a decent compromise.

But let's turn this into a functional style. We're iterating through a list and aggregating a result. That's a good job for reduce.

function average(numbers) {
  return _.reduce(numbers, (a, b) => a + b, 0) / (numbers.length || 1);
}

Very nice! Calling reduce with + and 0 could be called sum. Let's refactor:

function sum(numbers) {
  return _.reduce(numbers, (a, b) => a + b, 0);
}

function average(numbers) {
  return sum(numbers) / (numbers.length || 1);
}

Now that's declarative! It almost reads like the English-language definition for average.

Now let's tackle the Moving Average. Given a list of numbers, you take a "window" around each number and calculate the average of the numbers in that window.

function moving_average(numbers) {
  return _.chain(numbers).map(window).map(average).value();
}

We can read it like this: for each number in numbers, create a window, average the window, and return the list of averages. We have the function average above. But what about window?

function window(_number, index, array) {
  return _.slice(
    array,
    Math.max(0, index - 3),
    Math.min(array.length, index + 3)
  );
}

Maybe that one-liner is hard to read. We'll give the parts some names:

function window(_number, index, array) {
  const start = Math.max(0, index - 3);
  const end = Math.min(array.length, index + 3);
  return _.slice(array, start, end);
}

Let's see it all together:

function sum(numbers) {
  return _.reduce(numbers, (a, b) => a + b, 0);
}

function average(numbers) {
  return sum(numbers) / (numbers.length || 1);
}

function window(_number, index, array) {
  const start = Math.max(0, index - 3);
  const end = Math.min(array.length, index + 3);
  return _.slice(array, start, end);
}

function moving_average(numbers) {
  return _.chain(numbers).map(window).map(average).value();
}

Okay! That works. But now I want to be able to change the window. Right now, the window is 6 elements wide. Our boss is asking if we can change it. And if we need to change it once, we probably will need to do so again in the future. Let's anticipate that and make window more flexible:

function make_window(before, after) {
  return function (_number, index, array) {
    const start = Math.max(0, index - before);
    const end = Math.min(array.length, index + after + 1);
    return _.slice(array, start, end);
  };
}

I changed it slightly: notice that I add index+ after + 1, where before there was no + 1. It just seems to make more sense with the names before and after.

Now how does moving_average look?

function moving_average(numbers) {
  return _.chain(numbers).map(make_window(3, 2)).map(average).value();
}

Or we can pass the arguments in:

function moving_average(before, after, numbers) {
  return _.chain(numbers).map(make_window(before, after)).map(average).value();
}
Sean Allen
Sean Allen
Your friendly reminder that if you aren't reading Eric's newsletter, you are missing out…
👍 ❤️
Nicolas Hery
Nicolas Hery
Lots of great content in the latest newsletter! Really glad I subscribed. Thanks, Eric, for your work.
👍 ❤️
Mathieu Gagnon
Mathieu Gagnon
Eric's newsletter is so simply great. Love it!
👍 ❤️