-3

Based on this example, I want to group by object in a slightly other way. The outcome should be as follows:

[{
  key: "audi"
  items: [
    {
      "make": "audi",
      "model": "r8",
      "year": "2012"
    },
    {
      "make": "audi",
      "model": "rs5",
      "year": "2013"
    }
  ]
},
...
]

How can I achieve that? The following code I wrote doesn't do what I want:

reduce(function (r, a) {
        r[a.art] = {key: r[a.art], items: []} || [];
        r[a.art].items.push(a);
        return r;
    }, Object.create(null));
André R.
  • 427
  • 7
  • 17
  • 1
    what is the data you're starting with? what have you tried and what was your error? – sauntimo Jul 20 '17 at 16:52
  • My problem is, that I don't quite understand how the reduction works in the original example. I tried the following code, but it creates a pretty weird result: `reduce(function (r, a) { r[a.art] = {key: r[a.art], items: []} || []; r[a.art].items.push(a); return r; }, Object.create(null));` – André R. Jul 20 '17 at 16:53
  • Welcome to Stack Overflow! You seem to be asking for someone to write some code for you. Stack Overflow is a question and answer site, not a code-writing service. Please [see here](http://stackoverflow.com/help/how-to-ask) to learn how to write effective questions. – Rodrigo Leite Jul 20 '17 at 16:54
  • I figured you were using the same data as in the example you posted, but then I see your code looks for `a.art`, which I have no idea what it is, so I was unable to edit the starting data into your question, and you will have to do that. – James Jul 20 '17 at 16:58
  • @James: Yes, I copied it from my own code, which uses art instead of make. Sorry ^^ – André R. Jul 21 '17 at 08:40

2 Answers2

3

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 Scholz
  • 376,160
  • 25
  • 347
  • 392
3

@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))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • great your functional understanding, where i am lacking ... :/ mabe you could help with (not here in the question, but general) how to use monads in js. mabe you have an enlightend link or so. – Nina Scholz Jul 26 '17 at 11:03
  • 1
    Nina, I have many [answers that talk about monads](https://stackoverflow.com/search?q=user%3A633183+monad), almost all using JS. Much of my learning stemmed from [Mostly Adequate Guide](https://github.com/MostlyAdequate/mostly-adequate-guide) by [Brian Lonsdorf](https://twitter.com/drboolean?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor). He also [has some stuff on egghead](https://egghead.io/instructors/brian-lonsdorf) and [youtube](https://www.youtube.com/results?search_query=brian+lonsdorf) which I found to be very helpful. If you have more questions, I'm happy to help <3 – Mulan Jul 26 '17 at 22:23