Reducing/Grouping an array in Javascript

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-02 14:51:11

You could use a hash table for grouping by make and an array for the wanted result.

For every group in hash, a new object, like

{
    key: a.make,
    items: []
}

is created and pushed to the result set.

The hash table is initialized with a really empty object. There are no prototypes, to prevent collision.

var cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }],
    hash = Object.create(null),
    result = [];

cars.forEach(function (a) {
    if (!hash[a.make]) {
        hash[a.make] = { key: a.make, items: [] };
        result.push(hash[a.make]);
    }
    hash[a.make].items.push(a);
});

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

@Nina's answer is practical, efficient, and definitely the answer you should be reading. However, these problems are interesting to me and I like thinking about solving them in other ways, even if that means making trades.

Compound data equality in JavaScript

Testing for compound data equality in JavaScript can be a bother

console.log (1 === 1)         // true
console.log ('a' === 'a')     // true
console.log ([1,2] === [1,2]) // false
console.log ({a:1} === {a:1}) // false

This simple-natured equality test can make it somewhat challenging to deal with JavaScript's native Set and Map too

const m = new Map ()
m.set ([1,2], 'hello')
console.log (m.get ([1,2]))                      // undefined
console.log (m.get (Array.from (m.keys ()) [0])) // 'hello'

Bugger! Compound data equality bit us again. m.get cannot find the key [1,2] because the first key (that we set) [1,2] is different from the second key (to get) [1,2] – ie, the two instances of [1,2] are in different memory locations and are therefore considered (by JavaScript) to be inequal (!==)


Compound data equality, take 2

We don't have to play by JavaScript's rules, if we don't want to. In this part of the answer, we make our own Dict (dictionary) compound data type that accepts a function that is used to determine key equality

Imagine Dict working something like this

const d = Dict (({a} => a)
d.has ({a:1}) // false
d.set ({a:1}, 'hello') .has ({a:1}) // true
d.set ({a:1}, 'hello') .get ({a:1}) // 'hello'
d.get ({a:2}) // undefined
d.set ({a:2}, 'world') .get ({a:2}) // 'world'

If we had a data type that worked like Dict, then we could easily write the necessary transformation for our data

// our Dict type with custom key comparator
const DictByMake =
  Dict (x => x.make)

const dict =
  data.reduce((d, item) =>
    d.set (item, d.has (item)
      ? d.get (item) .concat ([item])
      : [item]), DictByMake ())

I say if we had a data type like Dict because it's good to be optimistic. Why should I make sacrifices and pick a data type incapable of fulfilling my needs when I don't have to? If a type I need doesn't exist, I can just make one. Thanks in advance, JavaScript !

Below I implement Dict with some consistency to JS's Set and Map – the most notable difference here is Dict is persistent (immutable) (a matter of preference, in this case)

const Pair = (left, right) => ({
  left,
  right
})

const Dict = eq => (pairs=[]) => ({
  equals (x, y) {
    return eq (x) === eq (y)
  },
  has (k) {
    for (const {left} of pairs)
      if (this.equals (k, left))
        return true
    return false
  },
  get (k) {
    for (const {left, right} of pairs)
      if (this.equals (k, left))
        return right
    return undefined
  },
  set (k, v) {
    for (const [i, {left, right}] of pairs.entries ())
      if (this.equals (k, left))
        return Dict (eq) (pairs
          .slice (0, i)
          .concat ([Pair (k, v)])
          .concat (pairs.slice (i+1)))
    return Dict (eq) (pairs.concat ([Pair (k, v)]))
  },
  entries () {
    return {
      *[Symbol.iterator] () {
        for (const {left, right} of pairs)
          yield [eq (left), right]
      }
    }
  }
})

const DictByMake =
  Dict (x => x.make)

const main = data => {
  // build the dict
  const dict =
    data.reduce((d, x) =>
      d.set(x, d.has (x)
        ? [...d.get (x), x]
        : [x]), DictByMake ())
  // convert dict key/value pairs to desired {key, items} shape
  return Array.from (dict.entries (), ([key, items]) =>
      ({ key, items }))
} 

const data = 
  [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
  
console.log (main (data))

Compound data equality, take 3

OK, that was a little intense inventing our very own data type ! In the example above, we based Dict off of an Array (native) of pairs – this naïve implementation detail makes Dict inefficient compared to other associative types that instead use a binary search tree or hash table to get/set keys. I used this an example to show how to build a more complex type from a more primitive one, but we could've just as easily made our own Tree type and used that instead.

In reality, we are given Map by JavaScript and don't have (get) to worry about how it's implemented – and while it doesn't have the exact behavior we want, we can adapt its behavior slightly without having to invent an entirely new type from scratch.

Worth noting, MapBy is not implemented as a persistent structure here

const MapBy = ord => (map = new Map ()) => ({
  has: k =>
    map.has (ord (k)),
  get: k =>
    map.get (ord (k)),
  set: (k, v) =>
    MapBy (ord) (map.set (ord (k), v)),
  keys: () =>
    map.keys (),
  values: () =>
    map.values (),
  entries: () =>
    map.entries ()
})

// the rest of the program stays exactly the same (with exception to variable names)
const MapByMake =
  MapBy (x => x.make)

const main = data => {
  const map =
    data.reduce((m, x) =>
      m.set(x, m.has (x)
        ? [...m.get (x), x]
        : [x]), MapByMake ())
  return Array.from (map.entries (), ([key, items]) =>
      ({ key, items }))
} 

const data = 
  [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
      
console.log (main (data))
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!