Using map and filter (not reduce) to get unique values of array (JavaScript)

#dev, #javascript

I have data like this:

[
  { "period": "2019-02-01", value: "10" },
  { "period": "2019-01-01", value: "1" },
  { "period": "2018-01-01", value: "2" },
  { "period": "2017-01-01", value: "3" },
]

There are actually 173 values in this data source. But this will work as an example.

And my problem is that I need to extract the maximum and minimum year OR all the years of this data source.

This is my first solution:

function getYears(records) {
  if (records.length > 0) {
    let result = records.map(record => {
      return parseInt(record.period.split("-")[0])
    })

    let current
    let arr2 = []

    do {
      current = result.pop()
      if (arr2.indexOf(current) == -1) {
        arr2.push(current)
      }
    } while (result.length > 0)

    return arr2
  } else {
    return []
  }
}

const data = [
  { "period": "2019-02-01", value: "10" },
  { "period": "2019-01-01", value: "1" },
  { "period": "2018-01-01", value: "2" },
  { "period": "2017-01-01", value: "3" },
]

console.log(getYears(data)) // => [ 2019, 2018, 2017 ]

But when I was writing this code I remember that someone told me something like this:

I almost never use for loop.

🤔

My background and the background of 100% (except for that one) is to use a for (or any other) loop first and then try to do something like map/filter.

So, I tried a more functional way to do it. I found this code. I read it. I tried to use in my example, but I did not get it at all. I had to see what was going on inside the loop (using console.log) to finally understand it.

This is the final code:

const data = [
  { "period": "2019-02-01", value: "10" },
  { "period": "2019-01-01", value: "1" },
  { "period": "2018-01-01", value: "2" },
  { "period": "2017-01-01", value: "3" },
]

const result = data.map(record => {
  return record.period.split("-")[0]
}).filter((elem, index, self) => {
  return index == self.indexOf(elem)
})

console.log(result) // => [ '2019', '2018', '2017' ]

Less code returns the same data. Now how it works:

  • indexOf returns the first index of an element inside an array;
  • map works as a way to get the year of my data, nothing new here;
  • filter returns only the elements which the block is true;

You got it? No? Here it is:

filter will filter the array of years and return only the element which the position is the first one in the array.

Don’t worry it took me a long time to come to this phrase. Let’s see how it works in our small example:

[
  { "period": "2019-02-01", value: "10" },
  { "period": "2019-01-01", value: "1" },
  { "period": "2018-01-01", value: "2" },
  { "period": "2017-01-01", value: "3" },
]

Our map on top of this example will return:

[ 2019, 2019, 2018, 2017 ]

When we run that filter code this is what happens:

  1. First item 2019: the index of this item is 0 let’s return it because the indexOf(2019) is 0 as well (remember: indexOf will return only the first index of 2019);
  2. Second item 2019: the index of this item is 1 let’s NOT return it because the indexOf(2019) is 0;
  3. Third item 2018: the index of this item is 2 let’s return it because the indexOf(2018) is 2;
  4. Fourth item 2017: the index of this item is 3 let’s return it because the indexOf(2017) is 3;

Benchmark

Ok, but “not only of code lives the man” 🤔.

Let’s do my second favourite thing to do when solving a problem: benchmark \o/.

I am using benchmark.js. This is the code for that:

// npm install microtime benchmark
let Benchmark = require('benchmark');
let suite = new Benchmark.Suite;
let data = require("./data.json")

suite.add("map+filter", function() {
  const result = data.map(record => {
    return record.period.split("-")[0]
  }).filter((elem, index, self) => {
    return index == self.indexOf(elem)
  })
})
.add("do+while", function() {
  if (data.length > 0) {
    let result = records.map(record => {
      return parseInt(record.period.split("-")[0])
    })

    let current
    let arr2 = []

    do {
      current = result.pop()
      if (arr2.indexOf(current) == -1) {
        arr2.push(current)
      }
    } while (result.length > 0)

    return arr2
  } else {
    return []
  }
})
.on("cycle", function(event) {
  console.log(String(event.target));
})
.on("complete", function() {
  console.log("Fastest is " + this.filter("fastest").map("name"));
})
.run({ "async": true });

data.json is a file with 173 items. I can not share it because it might contain sensitve data.

This is the result:

$ node benchmark.js
map+filter x 7,859 ops/sec ±1.19% (88 runs sampled)
do+while:
Fastest is map+filter

map+filter is not only cleaner but also faster 😉.

References