Categories
arrays group-by javascript object underscore.js

Most efficient method to groupby on an array of objects

811

What is the most efficient way to groupby objects in an array?

For example, given this array of objects:

[ 
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
]

I’m displaying this information in a table. I’d like to groupby different methods, but I want to sum the values.

I’m using Underscore.js for its groupby function, which is helpful, but doesn’t do the whole trick, because I don’t want them “split up” but “merged”, more like the SQL group by method.

What I’m looking for would be able to total specific values (if requested).

So if I did groupby Phase, I’d want to receive:

[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

And if I did groupy Phase / Step, I’d receive:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

Is there a helpful script for this, or should I stick to using Underscore.js, and then looping through the resulting object to do the totals myself?

2

  • While _.groupBy doesn’t do the job by itself, it can be combined with other Underscore functions to do what is asked. No manual loop required. See this answer: stackoverflow.com/a/66112210/1166087.

    – Julian

    Feb 9, 2021 at 2:45

  • A bit more readable version of the accepted answer: ­ function groupBy(data, key){ return data.reduce( (acc, cur) => { acc[cur[key]] = acc[cur[key]] || []; // if the key is new, initiate its value to an array, otherwise keep its own array value acc[cur[key]].push(cur); return acc; } , []) }

    – aderchox

    Jan 18 at 9:26


1157

If you want to avoid external libraries, you can concisely implement a vanilla version of groupBy() like so:

var groupBy = function(xs, key) {
  return xs.reduce(function(rv, x) {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});
};

console.log(groupBy(['one', 'two', 'three'], 'length'));

// => {3: ["one", "two"], 5: ["three"]}

27

  • 25

    i would modify this way : “` return xs.reduce(function(rv, x) { var v = key instanceof Function ? key(x) : x[key]; (rv[v] = rv[v] || []).push(x); return rv; }, {}); “` allowing callback functions to return a sorting criteria

    – y_nk

    Jul 6, 2016 at 16:50

  • 139

    Here is one that outputs array and not object: groupByArray(xs, key) { return xs.reduce(function (rv, x) { let v = key instanceof Function ? key(x) : x[key]; let el = rv.find((r) => r && r.key === v); if (el) { el.values.push(x); } else { rv.push({ key: v, values: [x] }); } return rv; }, []); }

    Aug 3, 2016 at 10:54

  • 46

    Great, just what i needed. In case anyone else needs it, here’s the TypeScript signature: var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...

    Dec 7, 2017 at 9:47


  • 47

    If anyone is interested, I made a more readable and annotated version of this function and put it in a gist: gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d I found it helpful for understanding what’s actually happening here.

    Oct 25, 2018 at 23:19


  • 50

    can’t we have sane variable names?

    – HJo

    Jul 1, 2019 at 11:27

369

Using ES6 Map object:

/**
 * @description
 * Takes an Array<V>, and a grouping function,
 * and returns a Map of the array grouped by the grouping function.
 *
 * @param list An array of type V.
 * @param keyGetter A Function that takes the the Array type V as an input, and returns a value of type K.
 *                  K is generally intended to be a property key of V.
 *
 * @returns Map of the array grouped by the grouping function.
 */
//export function groupBy<K, V>(list: Array<V>, keyGetter: (input: V) => K): Map<K, Array<V>> {
//    const map = new Map<K, Array<V>>();
function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
         const key = keyGetter(item);
         const collection = map.get(key);
         if (!collection) {
             map.set(key, [item]);
         } else {
             collection.push(item);
         }
    });
    return map;
}


// example usage

const pets = [
    {type:"Dog", name:"Spot"},
    {type:"Cat", name:"Tiger"},
    {type:"Dog", name:"Rover"}, 
    {type:"Cat", name:"Leo"}
];
    
const grouped = groupBy(pets, pet => pet.type);
    
console.log(grouped.get("Dog")); // -> [{type:"Dog", name:"Spot"}, {type:"Dog", name:"Rover"}]
console.log(grouped.get("Cat")); // -> [{type:"Cat", name:"Tiger"}, {type:"Cat", name:"Leo"}]

const odd = Symbol();
const even = Symbol();
const numbers = [1,2,3,4,5,6,7];

const oddEven = groupBy(numbers, x => (x % 2 === 1 ? odd : even));
    
console.log(oddEven.get(odd)); // -> [1,3,5,7]
console.log(oddEven.get(even)); // -> [2,4,6]

About Map:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

5

  • @mortb, how to get it without calling the get() method? which is I want the output is display without passing the key

    Dec 26, 2017 at 7:57

  • @FaiZalDong: I’m not sure what would be best for your case? If I write console.log(grouped.entries()); in the jsfiddle example it returns an iterable that is behaves like an array of keys + values. Can you try that and see if it helps?

    – mortb

    Dec 28, 2017 at 8:51

  • 8

    You could also try console.log(Array.from(grouped));

    – mortb

    Dec 28, 2017 at 10:32

  • 1

    to see the number of elements in groups: Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))

    Mar 12, 2019 at 10:03

  • I have transformed jsfiddle into stackoverflow inline code snippet. Original jsFiddle is still online at: jsfiddle.net/buko8r5d

    – mortb

    Mar 13, 2019 at 12:25

153

with ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);

5

  • 5

    It takes a bit to get used to, but so do most of C++ templates as well

    Jan 22, 2019 at 15:21


  • 10

    I wracked my brains and still failed to understand how in the world does it work starting from ...result. Now I can’t sleep because of that.

    – user3307073

    Mar 29, 2019 at 9:26


  • 15

    Elegant, but painfully slow on larger arrays!

    Apr 2, 2019 at 17:59

  • 2

    @user3307073 I think it looks at first glance like ...result is the starting value, which is why it’s so confusing (what is ...result if we haven’t started building result yet?). But starting value is is the second argument to .reduce(), not the first, and that’s down at the bottom: {}. So you always start with a JS object. Instead, ...result is in the {} that is passed to the first argument, so it means “start with all the fields you already had (before adding the new one item[key])”.

    Aug 13, 2020 at 21:07

  • 1

    @ArthurTacca you’re correct, result is the accumulator, meaning that it’s the “working value” that is updated by each item. It starts as the empty object, and each item is added to an array assigned to the property with the name of grouping field value.

    – Daniel

    Aug 4, 2021 at 15:41