Usando map e filter (não reduce) para listar valores únicos em array (JavaScript)

#dev, #javascript

Tenho uma estrutura semelhante a essa:

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

E meu problema é que eu preciso extrair o máximo e o mínimo OU todos os anos.

Esta é a minha primeira solução usando um loop do+while:

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 ]

Porém quando eu tava escrevendo esse código eu lembro que uma pessoa me falou que:

Eu quase nunca uso loop.

🤔

Meu background e o background de 100% (exceto por esta) é usar um loop for (ou qualquer outro) e só então tentar escrever de um jeito mais funcional.

Então, eu tentei escrever de um jeito mais funcional. I encontrei this code. Eu li. Eu tentei usar no meu exemplo. Mas eu não entendi nada. Eu só vi a entender quando eu coloquei debug no código com o console.log.

Este foi o resultado:

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' ]

Menos código, mas returna o mesmo resultado. Agora como funciona:

  • indexOf return o primeiro índice do elemento dentro de um array;
  • map está lá só para pegar a lista de todos os anos, nada novo aqui;
  • o filter retorna somente o(s) elemento(s) que o resultado do block seja true;

Entendeu? Não? Aqui está de uma outra forma:

filter filtra o array de anos e retorna somente o(s) elemento(s) se a posição dele for o primeiro dentro do array.

Não se preocupe eu levei um bom tempo para criar essa frase. Vamos ver como funciona para o nosso exemplo:

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

O map vai retornar os anos:

[ 2019, 2019, 2018, 2017 ]

Quanto ao filter isto é o que temos:

  1. O primeiro item 2019: o índice dele é 0 e ele vai ser retornado pois o indexOf(2019) também é 0. Lembre-se o indexOf retorna o índice do primeiro 2019;
  2. O segundo item 2019: o índice deste item é 1 e ele NÃO vai ser retornado pois o indexOf(2019) é 0;
  3. O terceiro item 2018: o índice deste item é 2 e ele vai ser retornado pois o indexOf(2018) também é 2;
  4. O quarto item é 2017: o índice deste item é 3 e ele vai ser retornado pois o indexOf(2017) também é 3;

Benchmark

Ok, mas “não somente de código vive o homem” 🤔.

Minha segunda parte favorita a se fazer quando eu tento resolver um problema é benchmark \o/.

Vou usar o benchmark.js. O código é esse:

// 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 é um arquivo com 173 itens. Eu não posso compartilhar, pois pode contar dados sensíveis.

Este é o resultado:

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

map+filter náo somente é mais curto de escrever, mas também é mais o rápido 😉.

Referências