904

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?

Paul Rooney
  • 20,879
  • 9
  • 40
  • 61
D'Arcy Rail-Ip
  • 11,505
  • 11
  • 42
  • 67
  • 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: https://stackoverflow.com/a/66112210/1166087. – Julian Feb 09 '21 at 02: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 '22 at 09:26

61 Answers61

1278

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"]}
Youp Bernoulli
  • 5,303
  • 5
  • 39
  • 59
Ceasar
  • 22,185
  • 15
  • 64
  • 83
  • 28
    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 06 '16 at 16:50
  • 154
    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; }, []); } – tomitrescak Aug 03 '16 at 10:54
  • 49
    Great, just what i needed. In case anyone else needs it, here's the TypeScript signature: `var groupBy = function(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...` – Michael Sandino Dec 07 '17 at 09:47
  • 6
    For what's it's worth, tomitrescak's solution, while convenient, is significantly less efficient, as find() is probably O(n). The solution in the answer is O(n), from the reduce (object assignment is O(1), as is push), whereas the comment is O(n)*O(n) or O(n^2) or at least O(nlgn) – narthur157 Jul 11 '18 at 10:47
  • great answer, just one tiny thing :), what if I want to group not by simple field but by a field that exist in another object, ex: ` [ { Phase: "Phase 1", Step: "Step 1", Value: {id: 1, value: 35} }, { Phase: "Phase 1", Step: "Step 2", Value: {id: 2, value: 55} }, { Phase: "Phase 2", Step: "Step 1", Value: {id: 1, value: 35} }, { Phase: "Phase 2", Step: "Step 2", Value: {id: 3, value: 75} } ] `?? – SlimenTN Aug 15 '18 at 09:41
  • 1
    @SlimenTN, you will need a more general solution. Some of the other answers in this thread might work better for you. – Ceasar Aug 15 '18 at 17:22
  • 1
    If you can use ES6, check out [the answer from Joseph Nields](https://stackoverflow.com/a/46431916/5763764). By using spread operators and computed property names you can arrive at a much more readable and elegant solution. – Radek Matěj Aug 24 '18 at 08:50
  • @Ceasar Bautista : I have to perform groupBy in ES6.. Can anyone suggestion same solution in ES6... – Asmi Sep 25 '18 at 09:19
  • 57
    If anyone is interested, I made a more readable and annotated version of this function and put it in a gist: https://gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d I found it helpful for understanding what's actually happening here. – robmathers Oct 25 '18 at 23:19
  • 86
    can't we have sane variable names? – HJo Jul 01 '19 at 11:27
  • 2
    @HamishJohnson it's a matter of style and experience. But `x`/`xs` is common in functional programming for variables that can be any type, and `rv` is common in Python as a shorthand for "return value". – Ceasar Jul 01 '19 at 23:45
  • How would you introduce multiple "key"s ? (group by multiple) – Efron A. Dec 17 '19 at 01:27
  • Isn't `rv[x[key]] = ` redundant here or I'm missing something? – Nikolay D Jan 08 '20 at 15:11
  • 1
    @Eron A. For multiple keys, this may do the job: `function groupBy (data, keys) { return data.reduce((result, r) => { var key = keys.map(k => r[k]).join(','); (result[key] = result[key] || []).push(r) return result }, {})` – Marshal Feb 21 '20 at 05:16
  • 2
    Replace `key` with `fn` and make it `rv[fn(x)]` instead of `rv[x[key]]` and you'll have a vastly more useful tool. Your use example then becomes `console.log(groupBy(['one', 'two', 'three'], a => a.length));` – Steven Armstrong Apr 16 '20 at 21:45
  • 2
    `xs.reduce((rv, x) => ((rv[x[key]] = rv[x[key]] || []).push(x), rv), {});` - simpler version with a [comma operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator) and [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) – lorond May 14 '20 at 12:39
  • Here's a version that takes an aggregation function, so that you can immediately get the summed values: https://gist.github.com/adesmet/ba8e6180239e77c73df56f27fdb61652. By default it stacks the values in a list, just like this answer does. – Anthony De Smet Jun 03 '20 at 17:41
  • Note: you assign to the value of the function parameter `rv` on each iteration. See elint issue page https://eslint.org/docs/rules/no-param-reassign – Joe Seifi Jul 29 '20 at 16:50
  • 1
    Is not rv[x[key]] = rv[x[key]] always true? I can't understand what rv carries. – igortp Nov 23 '20 at 12:02
  • Nice solution! One might even take @MichaelSandino's typings further and use stronger types for the key argument: ```var groupBy = function(xs: TItem[], key: keyof TItem) : {[key: string]: TItem[]} { ...``` – mitschmidt Feb 04 '21 at 11:43
  • map key/value pairs ===> `Object.entries(xs.reduce(function (rv, obj) { (rv[obj[keyId]] = rv[obj[keyId]] || []).push(obj); return rv; }, {})).map((value, key ) => ({ key, value: value }))` – Zanyar Jalal Jul 28 '21 at 07:36
  • I would add `delete item[key];` between (`rv[x[key]] = rv[x[key]] || []).push(x);` and `return rv;` – Arsenius Aug 09 '21 at 12:40
  • 1
    @igortp: no. My interpretation is that `rv[x[key]] = rv[x[key]] || []` means *assign* `rv[x[key]]` to `rv[x[key]]` resulting in the value of `rv[x[key]]`, *or if* `rv[x[key]]` is *empty* assign the value `[]` to `rv[x[key]]`. The intention would definitely have been clearer if the statement was written like `if(!`rv[x[key]]) { rv[x[key]] = [] }; rv[x[key]].push(x)`, but I guess the programmer wants to save ~10-15 characters to make the code to be as short as possible. – mortb Aug 31 '21 at 07:48
  • 1
    I don't understand the need for a reduce here since you keep returning the same modified object. A forEach that modifies that object would be more readable IMO. – Raphaël Verdier Sep 09 '21 at 09:02
  • If you want to return an array of objects use my way ```var groupByReturnArray = function (xs, key) { return xs.reduce(function (rv, x) { (rv[x[key]] = rv[x[key]] || []).push(x); return rv; }, []); ``` – Pixs Nguyen Sep 16 '21 at 02:39
  • I’m a bit wooly about this. In the question, the grouping by ‘phase’ results in value: 130 for Phase 2 for example. In this marked solution, it’s not clear to me how to automatically perform that summation on the numeric ‘value’ field for all occurrences of Phase 2 – user1729972 Nov 17 '21 at 22:34
  • Thank you @CeasarBautista, your code help me, to complete your answer I've wrote a complex example https://jsfiddle.net/m_farahmand/t59zhvqu/11/, may be help to someone else – M_Farahmand Jun 22 '22 at 06:56
  • How to fix this to full [Array.group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/group) specification, so that it works for non-array object too? – vitaly-t Dec 03 '22 at 10:24
  • https://gist.github.com/bhaireshm/3b0058025ba98b926292f4abf3bff243 refer this for multiple key sorting option. – Bhairesh M Apr 26 '23 at 09:12
409

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

mortb
  • 9,361
  • 3
  • 26
  • 44
  • @mortb, how to get it without calling the `get()` method? which is I want the output is display without passing the key – Fai Zal Dong Dec 26 '17 at 07: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 '17 at 08:51
  • 8
    You could also try `console.log(Array.from(grouped));` – mortb Dec 28 '17 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} ))` – Ahmet Şimşek Mar 12 '19 at 10:03
  • I have transformed jsfiddle into stackoverflow inline code snippet. Original jsFiddle is still online at: https://jsfiddle.net/buko8r5d/ – mortb Mar 13 '19 at 12:25
  • JSON.stringify(map) returns an empty array. So, if you need to stringify use object {} instead of Map – Omkar76 Oct 17 '22 at 10:18
  • @Omkar76: You can also use the method `Object.fromEntries(...)` and write : `JSON.stringify(Object.fromEntries(grouped));` if you want to get json from the `Map` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries – mortb Oct 17 '22 at 14:07
  • I like this solution! could you use also an indexed object as return type instead of a map? Something like `groupBy(list: V[], keyGetter: (input: V) => K): { [key: string]: V[] } { const groupedObject: { [key: string]: V[] } = {}; for (let item of list) { const key = String(keyGetter(item)); const collection = groupedObject[key]; if (!collection) groupedObject[key] = [item]; else groupedObject[key].push(item); }; return groupedObject; }` – Ansharja Dec 28 '22 at 12:19
  • 1
    @Ansharja yes you can use a javascript object instead of a `Map` as long as the key-getter function returns a string. Reading your code, I'd like to point out that there would be edge cases when just invoking the string constructor -- `String(keygetter)` -- to wrap the output in the keygetter function might lead to unintended consequences, for example when using a `Date` as a key. – mortb Jan 02 '23 at 13:43
166

with ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);
Joseph Nields
  • 5,527
  • 2
  • 32
  • 48
  • 5
    It takes a bit to get used to, but so do most of C++ templates as well – Levi Haskell Jan 22 '19 at 15:21
  • 11
    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. –  Mar 29 '19 at 09:26
  • 18
    Elegant, but painfully slow on larger arrays! – infinity1975 Apr 02 '19 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]`)". – Arthur Tacca Aug 13 '20 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 04 '21 at 15:41
125

You can build an ES6 Map from array.reduce().

const groupedMap = initialArray.reduce(
    (entryMap, e) => entryMap.set(e.id, [...entryMap.get(e.id)||[], e]),
    new Map()
);

This has a few advantages over the other solutions:

  • It doesn't require any libraries (unlike e.g. _.groupBy())
  • You get a JavaScript Map rather than an object (e.g. as returned by _.groupBy()). This has lots of benefits, including:
    • it remembers the order in which items were first added,
    • keys can be any type rather than just strings.
  • A Map is a more useful result that an array of arrays. But if you do want an array of arrays, you can then call Array.from(groupedMap.entries()) (for an array of [key, group array] pairs) or Array.from(groupedMap.values()) (for a simple array of arrays).
  • It's quite flexible; often, whatever you were planning to do next with this map can be done directly as part of the reduction.

As an example of the last point, imagine I have an array of objects that I want to do a (shallow) merge on by id, like this:

const objsToMerge = [{id: 1, name: "Steve"}, {id: 2, name: "Alice"}, {id: 1, age: 20}];
// The following variable should be created automatically
const mergedArray = [{id: 1, name: "Steve", age: 20}, {id: 2, name: "Alice"}]

To do this, I would usually start by grouping by id, and then merging each of the resulting arrays. Instead, you can do the merge directly in the reduce():

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);

Later edit:

The above is probably efficient enough for most purposes. But the original question was "most efficient" and, as a couple of people have pointed out, that's not true of the above solution. The problem is mainly that instantiates a new array for every entry. I had thought that this would be optimised away by the JS interpreter but it seems maybe not.

Someone suggested an edit to fix this but it really looked more complicated. Already the original snippet is pushing readability a little. If you really want to do this, please just use a for loop! It's not a sin! It takes one or two more lines of code but it's simpler than functional techniques even though it's not shorter:

const groupedMap = new Map();
for (const e of initialArray) {
    if (!groupedMap.has(e.id)) {
        groupedMap.set(e.id, []);
    }
    groupedMap.get(e.id).push(e);
}
Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49
  • 6
    I don't know why this doesn't have more votes. It's concise, readable (to me) and _looks_ efficient. [It doesn't fly on IE11](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Browser_compatibility), but the retrofit isn't too hard (`a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())`, approximately) – unbob Apr 25 '19 at 00:44
  • 3
    Because it's actually inefficient solution as it instantiates a new array at every reduce callback call. – Artem Balianytsia Sep 22 '22 at 17:09
99

GroupBy one-liner, an ES2021 solution

const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});

TypeScript

const groupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>
  array.reduce((acc, value, index, array) => {
    (acc[predicate(value, index, array)] ||= []).push(value);
    return acc;
  }, {} as { [key: string]: T[] });

EXAMPLES

const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});
// f -> should must return string/number because it will be use as key in object

// for demo

groupBy([1, 2, 3, 4, 5, 6, 7, 8, 9], v => (v % 2 ? "odd" : "even"));
// { odd: [1, 3, 5, 7, 9], even: [2, 4, 6, 8] };

const colors = [
  "Apricot",
  "Brown",
  "Burgundy",
  "Cerulean",
  "Peach",
  "Pear",
  "Red",
];

groupBy(colors, v => v[0]); // group by colors name first letter
// {
//   A: ["Apricot"],
//   B: ["Brown", "Burgundy"],
//   C: ["Cerulean"],
//   P: ["Peach", "Pear"],
//   R: ["Red"],
// };

groupBy(colors, v => v.length); // group by length of color names
// {
//   3: ["Red"],
//   4: ["Pear"],
//   5: ["Brown", "Peach"],
//   7: ["Apricot"],
//   8: ["Burgundy", "Cerulean"],
// }

const data = [
  { comment: "abc", forItem: 1, inModule: 1 },
  { comment: "pqr", forItem: 1, inModule: 1 },
  { comment: "klm", forItem: 1, inModule: 2 },
  { comment: "xyz", forItem: 1, inModule: 2 },
];

groupBy(data, v => v.inModule); // group by module
// {
//   1: [
//     { comment: "abc", forItem: 1, inModule: 1 },
//     { comment: "pqr", forItem: 1, inModule: 1 },
//   ],
//   2: [
//     { comment: "klm", forItem: 1, inModule: 2 },
//     { comment: "xyz", forItem: 1, inModule: 2 },
//   ],
// }

groupBy(data, x => x.forItem + "-" + x.inModule); // group by module with item
// {
//   "1-1": [
//     { comment: "abc", forItem: 1, inModule: 1 },
//     { comment: "pqr", forItem: 1, inModule: 1 },
//   ],
//   "1-2": [
//     { comment: "klm", forItem: 1, inModule: 2 },
//     { comment: "xyz", forItem: 1, inModule: 2 },
//   ],
// }

groupByToMap

const groupByToMap = (x, f) =>
  x.reduce((a, b, i, x) => {
    const k = f(b, i, x);
    a.get(k)?.push(b) ?? a.set(k, [b]);
    return a;
  }, new Map());

TypeScript

const groupByToMap = <T, Q>(array: T[], predicate: (value: T, index: number, array: T[]) => Q) =>
  array.reduce((map, value, index, array) => {
    const key = predicate(value, index, array);
    map.get(key)?.push(value) ?? map.set(key, [value]);
    return map;
  }, new Map<Q, T[]>());
Andrew Koper
  • 6,481
  • 6
  • 42
  • 50
nkitku
  • 4,779
  • 1
  • 31
  • 27
  • 1
    ||= is being rejected by my Babel? – Grant Aug 18 '21 at 02:46
  • 1
    That was just recently standardized. https://blog.saeloun.com/2021/06/17/es2021-logical-assignment-operator-and-or-nullish.html – loop Aug 19 '21 at 16:41
  • 2
    I love me my succinct takes-a-bit-more-time-to-figure-it-out magic one-liners! By far the most (subjectively) elegant solution. – Fung Oct 24 '21 at 04:48
  • 1
    Very elegant, especially being able to adjust the predicate in such a way. Gorgeous. – Micha Schopman Jan 21 '22 at 17:15
  • How to fix this to full [Array.group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/group) specification, so that it works for non-array object too? – vitaly-t Dec 03 '22 at 10:24
  • 1
    @vitaly-t If you want to use array like objects, you can use Array.from to convert to array or https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#calling_reduce_on_non-array_objects – nkitku Dec 04 '22 at 03:23
  • Can it group by multiple keys? – SkyWalker Jun 06 '23 at 16:43
  • @SkyWalker yes there is an example already check the last one – nkitku Jun 07 '23 at 09:53
79

I would check lodash groupBy it seems to do exactly what you are looking for. It is also quite lightweight and really simple.

Fiddle example: https://jsfiddle.net/r7szvt5k/

Provided that your array name is arr the groupBy with lodash is just:

import groupBy from 'lodash/groupBy';
// if you still use require:
// const groupBy = require('lodash/groupBy');

const a = groupBy(arr, function(n) {
  return n.Phase;
});
// a is your array grouped by Phase attribute
jmarceli
  • 19,102
  • 6
  • 69
  • 67
  • 4
    Isn't this answer problematic? There are multiple ways in which the lodash _.groupBy result is not in the format of the result the OP is requesting. (1) The result is not an array. (2) The "value" has become the "key" in the lodash object(s) result. – mg1075 Oct 24 '18 at 18:22
  • 1
    to make it simpler, you can just pass the attribute directly to groupBy: `const a = groupBy(arr, 'Phase')` – Ollie Nov 05 '20 at 21:54
63

Although the linq answer is interesting, it's also quite heavy-weight. My approach is somewhat different:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key, {Value: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.Value);
    }, 0)});
});

You can see it in action on JSBin.

I didn't see anything in Underscore that does what has does, although I might be missing it. It's much the same as _.contains, but uses _.isEqual rather than === for comparisons. Other than that, the rest of this is problem-specific, although with an attempt to be generic.

Now DataGrouper.sum(data, ["Phase"]) returns

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

And DataGrouper.sum(data, ["Phase", "Step"]) returns

[
    {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}
]

But sum is only one potential function here. You can register others as you like:

DataGrouper.register("max", function(item) {
    return _.extend({}, item.key, {Max: _.reduce(item.vals, function(memo, node) {
        return Math.max(memo, Number(node.Value));
    }, Number.NEGATIVE_INFINITY)});
});

and now DataGrouper.max(data, ["Phase", "Step"]) will return

[
    {Phase: "Phase 1", Step: "Step 1", Max: 10},
    {Phase: "Phase 1", Step: "Step 2", Max: 20},
    {Phase: "Phase 2", Step: "Step 1", Max: 30},
    {Phase: "Phase 2", Step: "Step 2", Max: 40}
]

or if you registered this:

DataGrouper.register("tasks", function(item) {
    return _.extend({}, item.key, {Tasks: _.map(item.vals, function(item) {
      return item.Task + " (" + item.Value + ")";
    }).join(", ")});
});

then calling DataGrouper.tasks(data, ["Phase", "Step"]) will get you

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

DataGrouper itself is a function. You can call it with your data and a list of the properties you want to group by. It returns an array whose elements are object with two properties: key is the collection of grouped properties, vals is an array of objects containing the remaining properties not in the key. For example, DataGrouper(data, ["Phase", "Step"]) will yield:

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

DataGrouper.register accepts a function and creates a new function which accepts the initial data and the properties to group by. This new function then takes the output format as above and runs your function against each of them in turn, returning a new array. The function that's generated is stored as a property of DataGrouper according to a name you supply and also returned if you just want a local reference.

Well that's a lot of explanation. The code is reasonably straightforward, I hope!

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
52

This is probably more easily done with linq.js, which is intended to be a true implementation of LINQ in JavaScript (DEMO):

var linq = Enumerable.From(data);
var result =
    linq.GroupBy(function(x){ return x.Phase; })
        .Select(function(x){
          return {
            Phase: x.Key(),
            Value: x.Sum(function(y){ return y.Value|0; })
          };
        }).ToArray();

result:

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

Or, more simply using the string-based selectors (DEMO):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();
Mark Milford
  • 1,180
  • 1
  • 15
  • 32
mellamokb
  • 56,094
  • 12
  • 110
  • 136
31

MDN has this example in their Array.reduce() documentation.

// Grouping objects by a property
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Grouping_objects_by_a_property#Grouping_objects_by_a_property

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }
HoppyKamper
  • 1,044
  • 9
  • 10
  • I am missing something, obviously. Why can't we produce an array of arrays with this solution by MDN? If you try ti initialise the reducer with ,[] you get an empty array as a result. – Stamatis Deliyannis Nov 09 '20 at 12:25
25
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

From: http://underscorejs.org/#groupBy

Julio Marins
  • 10,039
  • 8
  • 48
  • 54
25

it's a bit late but maybe someone like this one.

ES6:

const users = [{
    name: "Jim",
    color: "blue"
  },
  {
    name: "Sam",
    color: "blue"
  },
  {
    name: "Eddie",
    color: "green"
  },
  {
    name: "Robert",
    color: "green"
  },
];
const groupBy = (arr, key) => {
  const initialValue = {};
  return arr.reduce((acc, cval) => {
    const myAttribute = cval[key];
    acc[myAttribute] = [...(acc[myAttribute] || []), cval]
    return acc;
  }, initialValue);
};

const res = groupBy(users, "color");
console.log("group by:", res);
Kmylo darkstar
  • 359
  • 3
  • 3
  • 1
    Thank you your method works, I'm a bit new to this concept can you explain that initialValue part what does it did – Praveen Vishnu Oct 07 '21 at 01:53
  • @PraveenVishnu initialValue is part of reduce callback, I just wanted to add it explicitly https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce – Kmylo darkstar Apr 11 '22 at 07:40
  • another way to write this with ES6 is: ``` const groupBy = (arr, key) => { const initialValue = {}; const reducer = (acc, currentObj) => { const groupByKey = currentObj[key]; const currentGroup = acc[groupByKey] || []; return { ...acc, [groupByKey]: [...currentGroup, currentObj], }; }; return arr.reduce(reducer, initialValue); }; ``` – Kmylo darkstar May 05 '23 at 06:15
22
Array.prototype.groupBy = function(keyFunction) {
    var groups = {};
    this.forEach(function(el) {
        var key = keyFunction(el);
        if (key in groups == false) {
            groups[key] = [];
        }
        groups[key].push(el);
    });
    return Object.keys(groups).map(function(key) {
        return {
            key: key,
            values: groups[key]
        };
    });
};
cezarypiatek
  • 1,078
  • 11
  • 21
20

You can do it with Alasql JavaScript library:

var data = [ { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
             { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }];

var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \
                  FROM ? GROUP BY Phase, Step',[data]);

Try this example at jsFiddle.

BTW: On large arrays (100000 records and more) Alasql faster tham Linq. See test at jsPref.

Comments:

  • Here I put Value in square brackets, because VALUE is a keyword in SQL
  • I have to use CAST() function to convert string Values to number type.
agershun
  • 4,077
  • 38
  • 41
20

A newer approach with an object for grouping and two more function to create a key and to get an object with wanted items of grouping and another key for the adding value.

const
    groupBy = (array, groups, valueKey) => {
        const
            getKey = o => groups.map(k => o[k]).join('|'),
            getObject = o => Object.fromEntries([...groups.map(k => [k, o[k]]), [valueKey, 0]]);

        groups = [].concat(groups);

        return Object.values(array.reduce((r, o) => {
            (r[getKey(o)] ??= getObject(o))[valueKey] += +o[valueKey];
            return r;
        }, {}));
    },
    data = [{ 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" }];

console.log(groupBy(data, 'Phase', 'Value'));
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Old approach:

Although the question have some answers and the answers look a bit over complicated, I suggest to use vanilla Javascript for group-by with a nested (if necessary) Map.

function groupBy(array, groups, valueKey) {
    var map = new Map;
    groups = [].concat(groups);
    return array.reduce((r, o) => {
        groups.reduce((m, k, i, { length }) => {
            var child;
            if (m.has(o[k])) return m.get(o[k]);
            if (i + 1 === length) {
                child = Object
                    .assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 });
                r.push(child);
            } else {
                child = new Map;
            }
            m.set(o[k], child);
            return child;
        }, map)[valueKey] += +o[valueKey];
        return r;
    }, [])
};

var data = [{ 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" }];

console.log(groupBy(data, 'Phase', 'Value'));
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
15

Checked answer -- just shallow grouping. It's pretty nice to understand reducing. Question also provide the problem of additional aggregate calculations.

Here is a REAL GROUP BY for Array of Objects by some field(s) with 1) calculated key name and 2) complete solution for cascading of grouping by providing the list of the desired keys and converting its unique values to root keys like SQL GROUP BY does.

const inputArray = [ 
    { 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" }
];

var outObject = inputArray.reduce(function(a, e) {
  // GROUP BY estimated key (estKey), well, may be a just plain key
  // a -- Accumulator result object
  // e -- sequentally checked Element, the Element that is tested just at this itaration

  // new grouping name may be calculated, but must be based on real value of real field
  let estKey = (e['Phase']); 

  (a[estKey] ? a[estKey] : (a[estKey] = null || [])).push(e);
  return a;
}, {});

console.log(outObject);

Play with estKey -- you may group by more then one field, add additional aggregations, calculations or other processing.

Also you can groups data recursively. For example initially group by Phase, then by Step field and so on. Additionally blow off the fat rest data.

const inputArray = [
{ 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" }
  ];

/**
 * Small helper to get SHALLOW copy of obj WITHOUT prop
 */
const rmProp = (obj, prop) => ( (({[prop]:_, ...rest})=>rest)(obj) )

/**
 * Group Array by key. Root keys of a resulting array is value
 * of specified key.
 *
 * @param      {Array}   src     The source array
 * @param      {String}  key     The by key to group by
 * @return     {Object}          Object with grouped objects as values
 */
const grpBy = (src, key) => src.reduce((a, e) => (
  (a[e[key]] = a[e[key]] || []).push(rmProp(e, key)),  a
), {});

/**
 * Collapse array of object if it consists of only object with single value.
 * Replace it by the rest value.
 */
const blowObj = obj => Array.isArray(obj) && obj.length === 1 && Object.values(obj[0]).length === 1 ? Object.values(obj[0])[0] : obj;

/**
 * Recursive grouping with list of keys. `keyList` may be an array
 * of key names or comma separated list of key names whom UNIQUE values will
 * becomes the keys of the resulting object.
 */
const grpByReal = function (src, keyList) {
  const [key, ...rest] = Array.isArray(keyList) ? keyList : String(keyList).trim().split(/\s*,\s*/);
  const res = key ? grpBy(src, key) : [...src];
  if (rest.length) {
for (const k in res) {
  res[k] = grpByReal(res[k], rest)
}
  } else {
for (const k in res) {
  res[k] = blowObj(res[k])
}
  }
  return res;
}

console.log( JSON.stringify( grpByReal(inputArray, 'Phase, Step, Task'), null, 2 ) );
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
SynCap
  • 6,244
  • 2
  • 18
  • 27
14

Here's a nasty, hard to read solution using ES6:

export default (arr, key) => 
  arr.reduce(
    (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
    {}
  );

For those asking how does this even work, here's an explanation:

  • In both => you have a free return

  • The Array.prototype.reduce function takes up to 4 parameters. That's why a fifth parameter is being added so we can have a cheap variable declaration for the group (k) at the parameter declaration level by using a default value. (yes, this is sorcery)

  • If our current group doesn't exist on the previous iteration, we create a new empty array ((r[k] || (r[k] = [])) This will evaluate to the leftmost expression, in other words, an existing array or an empty array, this is why there's an immediate push after that expression, because either way you will get an array.

  • When there's a return, the comma , operator will discard the leftmost value, returning the tweaked previous group for this scenario.

An easier to understand version that does the same is:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});

Edit:

TS Version:

const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
  list.reduce((previous, currentItem) => {
    const group = getKey(currentItem);
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {} as Record<K, T[]>);
kevinrodriguez-io
  • 1,054
  • 9
  • 15
  • 2
    would you care to explain this a bit, it works perfect – Nuwan Dammika Oct 29 '19 at 22:05
  • 1
    @NuwanDammika - In both => you have a free "return" - The reduce function takes up to 4 parameters. That's why a fifth parameter is being added so we can have a cheap variable declaration for the group (k). - If the previous value doesn't have our current group, we create a new empty group ((r[k] || (r[k] = [])) This will evaluate to the leftmost expression, otherwise an array or an empty array, this is why there's an immediate push after that expression. - When there's a return, the comma operator will discard the leftmost value, returning the tweaked previous group. – kevinrodriguez-io May 26 '20 at 23:25
  • 3
    Best syntax for TS. Best answer, when using with complex objects. `const groups = groupBy(items, (x) => x.groupKey);` – Dmytro Sokhach Sep 02 '20 at 18:42
  • This is great. I'm a scala guy and feel right at home. Well.. except for what is the _default_ for? – WestCoastProjects Oct 09 '20 at 23:52
  • @javadba export default is just the syntax to be used with JS Modules, similar to just exporting, the default keyword will allow you to import like this: import Group from '../path/to/module'; – kevinrodriguez-io Oct 12 '20 at 16:48
8

i'd like to suggest my approach. First, separate grouping and aggregating. Lets declare prototypical "group by" function. It takes another function to produce "hash" string for each array element to group by.

Array.prototype.groupBy = function(hash){
  var _hash = hash ? hash : function(o){return o;};

  var _map = {};
  var put = function(map, key, value){
    if (!map[_hash(key)]) {
        map[_hash(key)] = {};
        map[_hash(key)].group = [];
        map[_hash(key)].key = key;

    }
    map[_hash(key)].group.push(value); 
  }

  this.map(function(obj){
    put(_map, obj, obj);
  });

  return Object.keys(_map).map(function(key){
    return {key: _map[key].key, group: _map[key].group};
  });
}

when grouping is done you can aggregate data how you need, in your case

data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});})
    /* aggreagating */
    .map(function(el){ 
         var sum = el.group.reduce(
           function(l,c){
             return l + parseInt(c.Value);
           },
           0
         );
         el.key.Value = sum; 
         return el.key;
    });

in common it works. i have tested this code in chrome console. and feel free to improve and find mistakes ;)

Anton
  • 2,535
  • 2
  • 25
  • 28
  • Thanks ! Love the approach, and suits my needs perfectly (I don't need aggregating). – aberaud Mar 02 '14 at 01:13
  • I think you want to change your line in put(): `map[_hash(key)].key = key;` to `map[_hash(key)].key = _hash(key);`. – Scotty.NET Mar 03 '15 at 13:46
  • Be aware that this is going to fail if the array contains strings with names similar to any function in object prototype (eg: `["toString"].groupBy()`) – tigrou Nov 03 '21 at 21:29
8
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;
    }, []);
}

This one outputs array.

tomitrescak
  • 1,072
  • 12
  • 22
8

Without mutations:

const groupBy = (xs, key) => xs.reduce((acc, x) => Object.assign({}, acc, {
  [x[key]]: (acc[x[key]] || []).concat(x)
}), {})

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}
Bless
  • 5,052
  • 2
  • 40
  • 44
8

This solution takes any arbitrary function (not a key) so it's more flexible than solutions above, and allows arrow functions, which are similar to lambda expressions used in LINQ:

Array.prototype.groupBy = function (funcProp) {
    return this.reduce(function (acc, val) {
        (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val);
        return acc;
    }, {});
};

NOTE: whether you want to extend Array's prototype is up to you.

Example supported in most browsers:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(function(c){return c.a;})

Example using arrow functions (ES6):

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(c=>c.a)

Both examples above return:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}
Diego
  • 18,035
  • 5
  • 62
  • 66
  • I liked a lot the ES6 solution. Just a little semplification without extending Array prototype: `let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});` – caneta Apr 28 '17 at 10:17
6

Imagine that you have something like this:

[{id:1, cat:'sedan'},{id:2, cat:'sport'},{id:3, cat:'sport'},{id:4, cat:'sedan'}]

By doing this: const categories = [...new Set(cars.map((car) => car.cat))]

You will get this: ['sedan','sport']

Explanation: 1. First, we are creating a new Set by passing an array. Because Set only allows unique values, all duplicates will be removed.

  1. Now the duplicates are gone, we’re going to convert it back to an array by using the spread operator ...

Set Doc:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread OperatorDoc: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

Yago Gehres
  • 69
  • 1
  • 4
  • i like your answer very much, it's the shortest one, but i still dont understand the logic, especially, who do the grouping here ? is it spread operator(...) ? or the 'new Set()' ? please explain it to us ... thank you – Ivan Feb 08 '20 at 16:08
  • 1
    1. First, we are creating a new Set by passing an array. Because Set only allows unique values, all duplicates will be removed. 2. Now the duplicates are gone, we’re going to convert it back to an array by using the spread operator ... Set Doc:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread Operator:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax – Yago Gehres Feb 09 '20 at 23:43
5

Based on previous answers

const groupBy = (prop) => (xs) =>
  xs.reduce((rv, x) =>
    Object.assign(rv, {[x[prop]]: [...(rv[x[prop]] || []), x]}), {});

and it's a little nicer to look at with object spread syntax, if your environment supports.

const groupBy = (prop) => (xs) =>
  xs.reduce((acc, x) => ({
    ...acc,
    [ x[ prop ] ]: [...( acc[ x[ prop ] ] || []), x],
  }), {});

Here, our reducer takes the partially-formed return value (starting with an empty object), and returns an object composed of the spread out members of the previous return value, along with a new member whose key is calculated from the current iteree's value at prop and whose value is a list of all values for that prop along with the current value.

Benny Powers
  • 5,398
  • 4
  • 32
  • 55
5

You can use native JavaScript group array method (currently in stage 2).

I think solution is much more elegant, compared to reduce, or reaching out for third-party libs such as lodash etc.

const products = [{
    name: "milk",
    type: "dairy"
  },
  {
    name: "cheese",
    type: "dairy"
  },
  {
    name: "beef",
    type: "meat"
  },
  {
    name: "chicken",
    type: "meat"
  }
];

const productsByType = products.group((product) => product.type);

console.log("Grouped products by type: ", productsByType);
<script src="https://cdn.jsdelivr.net/npm/core-js-bundle@3.23.2/minified.min.js"></script>
marko424
  • 3,839
  • 5
  • 17
  • 27
4

I don't think that given answers are responding to the question, I think this following should answer to the first part :

const arr = [ 
{ 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" }
]

const groupBy = (key) => arr.sort((a, b) => a[key].localeCompare(b[key])).reduce((total, currentValue) => {
  const newTotal = total;
  if (
    total.length &&
    total[total.length - 1][key] === currentValue[key]
  )
    newTotal[total.length - 1] = {
      ...total[total.length - 1],
      ...currentValue,
      Value: parseInt(total[total.length - 1].Value) + parseInt(currentValue.Value),
    };
  else newTotal[total.length] = currentValue;
  return newTotal;
}, []);

console.log(groupBy('Phase'));

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

console.log(groupBy('Step'));

// => [{ Step: "Step 1", Value: 70 },{ Step: "Step 2", Value: 110 }] 
Aznhar
  • 610
  • 1
  • 10
  • 30
4

groupBy function that can group an array by a specific key or a given grouping function. Typed.

groupBy = <T, K extends keyof T>(array: T[], groupOn: K | ((i: T) => string)): Record<string, T[]> => {
  const groupFn = typeof groupOn === 'function' ? groupOn : (o: T) => o[groupOn];

  return Object.fromEntries(
    array.reduce((acc, obj) => {
      const groupKey = groupFn(obj);
      return acc.set(groupKey, [...(acc.get(groupKey) || []), obj]);
    }, new Map())
  ) as Record<string, T[]>;
};
edin0x
  • 275
  • 3
  • 6
  • I would be interested about a perf benchmark of this version (with new array and destructuring at every round to create the value to be set) against another which create an empty array only when needed. Based on your code: https://gist.github.com/masonlouchart/da141b3af477ff04ccc626f188110f28 – Mason Apr 09 '21 at 14:38
  • Just to be clear, for newbies stumbling onto this, this is Typescript code, and the original question was tagged javascript, so this is rather off topic, right? – Neek Oct 12 '21 at 03:50
3

Array.prototype.groupBy = function (groupingKeyFn) {
    if (typeof groupingKeyFn !== 'function') {
        throw new Error("groupBy take a function as only parameter");
    }
    return this.reduce((result, item) => {
        let key = groupingKeyFn(item);
        if (!result[key])
            result[key] = [];
        result[key].push(item);
        return result;
    }, {});
}

var a = [
 {type: "video", name: "a"},
  {type: "image", name: "b"},
  {type: "video", name: "c"},
  {type: "blog", name: "d"},
  {type: "video", name: "e"},
]
console.log(a.groupBy((item) => item.type));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Jean-Philippe
  • 397
  • 4
  • 5
3

I would check declarative-js groupBy it seems to do exactly what you are looking for. It is also:

  • very performant (performance benchmark)
  • written in typescript so all typpings are included.
  • It is not enforcing to use 3rd party array-like objects.
import { Reducers } from 'declarative-js';
import groupBy = Reducers.groupBy;
import Map = Reducers.Map;

const data = [
    { 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" }
];

data.reduce(groupBy(element=> element.Step), Map());
data.reduce(groupBy('Step'), Map());
Error404
  • 719
  • 9
  • 30
Pasa89
  • 31
  • 2
3

Let's fully answer the original question while reusing code that was already written (i.e., Underscore). You can do much more with Underscore if you combine its >100 functions. The following solution demonstrates this.

Step 1: group the objects in the array by an arbitrary combination of properties. This uses the fact that _.groupBy accepts a function that returns the group of an object. It also uses _.chain, _.pick, _.values, _.join and _.value. Note that _.value is not strictly needed here, because chained values will automatically unwrap when used as a property name. I'm including it to safeguard against confusion in case somebody tries to write similar code in a context where automatic unwrapping does not take place.

// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

Given the arrayOfObjects in the original question and setting propertyNames to ['Phase', 'Step'], intermediate will get the following value:

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

Step 2: reduce each group to a single flat object and return the results in an array. Besides the functions we have seen before, the following code uses _.pluck, _.first, _.pick, _.extend, _.reduce and _.map. _.first is guaranteed to return an object in this case, because _.groupBy does not produce empty groups. _.value is necessary in this case.

// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

Given the intermediate that we obtained before and setting aggregateProperty to Value, we get the result that the asker desired:

[
    { 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 }
]

We can put this all together in a function that takes arrayOfObjects, propertyNames and aggregateProperty as parameters. Note that arrayOfObjects can actually also be a plain object with string keys, because _.groupBy accepts either. For this reason, I have renamed arrayOfObjects to collection.

function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects, ['Phase', 'Step'], 'Value') will now give us the same result again.

We can take this a step further and enable the caller to compute any statistic over the values in each group. We can do this and also enable the caller to add arbitrary properties to the summary of each group. We can do all of this while making our code shorter. We replace the aggregateProperty parameter by an iteratee parameter and pass this straight to _.reduce:

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

In effect, we move some of the responsibility to the caller; she must provide an iteratee that can be passed to _.reduce, so that the call to _.reduce will produce an object with the aggregate properties she wants to add. For example, we obtain the same result as before with the following expression:

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

For an example of a slightly more sophisticated iteratee, suppose that we want to compute the maximum Value of each group instead of the sum, and that we want to add a Tasks property that lists all the values of Task that occur in the group. Here's one way we can do this, using the last version of aggregate above (and _.union):

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

We obtain the following result:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

Credit to @much2learn, who also posted an answer that can handle arbitrary reducing functions. I wrote a couple more SO answers that demonstrate how one can achieve sophisticated things by combining multiple Underscore functions:

Julian
  • 4,176
  • 19
  • 40
3

Explain the same code. like it I fond it here

const groupBy = (array, key) => {
  return array.reduce((result, currentValue) => {
    (result[currentValue[key]] = result[currentValue[key]] || []).push(
      currentValue
    );
    console.log(result);
    return result;
  }, {});
};

USE

 let group =   groupBy(persons, 'color');
Carlos
  • 572
  • 1
  • 5
  • 13
2

Lets generate a generic Array.prototype.groupBy() tool. Just for variety let's use ES6 fanciness the spread operator for some Haskellesque pattern matching on a recursive approach. Also let's make our Array.prototype.groupBy() to accept a callback which takes the item (e) the index (i) and the applied array (a) as arguments.

Array.prototype.groupBy = function(cb){
                            return function iterate([x,...xs], i = 0, r = [[],[]]){
                                     cb(x,i,[x,...xs]) ? (r[0].push(x), r)
                                                       : (r[1].push(x), r);
                                     return xs.length ? iterate(xs, ++i, r) : r;
                                   }(this);
                          };

var arr = [0,1,2,3,4,5,6,7,8,9],
    res = arr.groupBy(e => e < 5);
console.log(res);
Redu
  • 25,060
  • 6
  • 56
  • 76
2

Ceasar's answer is good, but works only for inner properties of the elements inside the array (length in case of string).

this implementation works more like: this link

const groupBy = function (arr, f) {
    return arr.reduce((out, val) => {
        let by = typeof f === 'function' ? '' + f(val) : val[f];
        (out[by] = out[by] || []).push(val);
        return out;
    }, {});
};

hope this helps...

Hasan Fathi
  • 5,610
  • 4
  • 42
  • 60
Roey
  • 1,647
  • 21
  • 16
2

Here is a ES6 version that won't break on null members

function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}
bigkahunaburger
  • 426
  • 5
  • 10
2

Just to add to Scott Sauyet's answer, some people were asking in the comments how to use his function to groupby value1, value2, etc., instead of grouping just one value.

All it takes is to edit his sum function:

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key,
        {VALUE1: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.VALUE1);}, 0)},
        {VALUE2: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.VALUE2);}, 0)}
    );
});

leaving the main one (DataGrouper) unchanged:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());
Telho
  • 21
  • 5
2

From @mortb, @jmarceli answer and from this post,

I take the advantage of JSON.stringify() to be the identity for the PRIMITIVE VALUE multiple columns of group by.

Without third-party

function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
        const key = keyGetter(item);
        if (!map.has(key)) {
            map.set(key, [item]);
        } else {
            map.get(key).push(item);
        }
    });
    return map;
}

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

const grouped = groupBy(pets,
pet => JSON.stringify({ type: pet.type, age: pet.age }));

console.log(grouped);

With Lodash third-party

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

let rslt = _.groupBy(pets, pet => JSON.stringify(
 { type: pet.type, age: pet.age }));

console.log(rslt);
Pranithan T.
  • 323
  • 1
  • 6
  • 14
  • keyGetter returns undefined – Asbar Ali May 12 '18 at 15:09
  • @AsbarAli I tested my snippet with Chrome's console - Version 66.0.3359.139 (Official Build) (64-bit). And everything runs fine. Could you please put the debugging break point and see why keyGetter is undefined. It is, perhaps, because of browser version. – Pranithan T. May 13 '18 at 16:43
2

ES6 reduce based version version with the support for function iteratee.

Works just as expected if the iteratee function is not provided:

const data = [{id: 1, score: 2},{id: 1, score: 3},{id: 2, score: 2},{id: 2, score: 4}]

const group = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});

const groupBy = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(group(data, 'id'))     // grouping via `reduce`
console.log(groupBy(data, 'id'))   // same result if `fn` is omitted
console.log(groupBy(data, 'score', x => x > 2 )) // group with the iteratee

In the context of the OP question:

const data = [ { 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" } ]

const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});
const groupWith = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(groupBy(data, 'Phase'))
console.log(groupWith(data, 'Value', x => x > 30 ))  // group by `Value` > 30

Another ES6 version which reverses the grouping and uses the values as keys and the keys as the grouped values:

const data = [{A: "1"}, {B: "10"}, {C: "10"}]

const groupKeys = arr => 
  arr.reduce((r,c) => (Object.keys(c).map(x => r[c[x]] = [...r[c[x]] || [], x]),r),{});

console.log(groupKeys(data))

Note: functions are posted in their short form (one line) for brevity and to relate just the idea. You can expand them and add additional error checking etc.

Akrion
  • 18,117
  • 1
  • 34
  • 54
2

Usually I use Lodash JavaScript utility library with a pre-built groupBy() method. It is pretty easy to use, see more details here.

Kate Orlova
  • 3,225
  • 5
  • 11
  • 35
Ping Woo
  • 1,423
  • 15
  • 21
2

I have improved answers. This function takes array of group fields and return grouped object whom key is also object of group fields.

function(xs, groupFields) {
        groupFields = [].concat(groupFields);
        return xs.reduce(function(rv, x) {
            let groupKey = groupFields.reduce((keyObject, field) => {
                keyObject[field] = x[field];
                return keyObject;
            }, {});
            (rv[JSON.stringify(groupKey)] = rv[JSON.stringify(groupKey)] || []).push(x);
            return rv;
        }, {});
    }



let x = [
{
    "id":1,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":2,
    "multimedia":false,
    "language":["fr"]
},
{
    "id":3,
    "multimedia":true,
    "language":["tr"]
},
{
    "id":4,
    "multimedia":false,
    "language":[]
},
{
    "id":5,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":6,
    "multimedia":false,
    "language":["tr"]
},
{
    "id":7,
    "multimedia":false,
    "language":["tr","fr"]
}
]

groupBy(x, ['multimedia','language'])

//{
//{"multimedia":false,"language":["tr"]}: Array(3), 
//{"multimedia":false,"language":["fr"]}: Array(1), 
//{"multimedia":true,"language":["tr"]}: Array(1), 
//{"multimedia":false,"language":[]}: Array(1), 
//{"multimedia":false,"language":["tr","fr"]}: Array(1)
//}
Gürcan Kavakçı
  • 562
  • 1
  • 11
  • 24
2

Posting because even though this question is 7 years old, I have yet to see an answer that satisfies the original criteria:

I don’t want them “split up” but “merged”, more like the SQL group by method.

I originally came to this post because I wanted to find a method of reducing an array of objects (i.e., the data structure created when you read from a csv, for example) and aggregate by given indices to produce the same data structure. The return value I was looking for was another array of objects, not a nested object or map like I've seen proposed here.

The following function takes a dataset (array of objects), a list of indices (array), and a reducer function, and returns the result of applying the reducer function on the indices as an array of objects.

function agg(data, indices, reducer) {

  // helper to create unique index as an array
  function getUniqueIndexHash(row, indices) {
    return indices.reduce((acc, curr) => acc + row[curr], "");
  }

  // reduce data to single object, whose values will be each of the new rows
  // structure is an object whose values are arrays
  // [{}] -> {{}}
  // no operation performed, simply grouping
  let groupedObj = data.reduce((acc, curr) => {
    let currIndex = getUniqueIndexHash(curr, indices);

    // if key does not exist, create array with current row
    if (!Object.keys(acc).includes(currIndex)) {
      acc = {...acc, [currIndex]: [curr]}
    // otherwise, extend the array at currIndex
    } else {
      acc = {...acc, [currIndex]: acc[currIndex].concat(curr)};
    }

    return acc;
  }, {})

  // reduce the array into a single object by applying the reducer
  let reduced = Object.values(groupedObj).map(arr => {
    // for each sub-array, reduce into single object using the reducer function
    let reduceValues = arr.reduce(reducer, {});

    // reducer returns simply the aggregates - add in the indices here
    // each of the objects in "arr" has the same indices, so we take the first
    let indexObj = indices.reduce((acc, curr) => {
      acc = {...acc, [curr]: arr[0][curr]};
      return acc;
    }, {});

    reduceValues = {...indexObj, ...reduceValues};


    return reduceValues;
  });


  return reduced;
}

I'll create a reducer that returns count(*) and sum(Value):

reducer = (acc, curr) => {
  acc.count = 1 + (acc.count || 0);
  acc.value = +curr.Value + (acc.value|| 0);
  return acc;
}

finally, applying the agg function with our reducer to the original dataset yields an array of objects with the appropriate aggregations applied:

agg(tasks, ["Phase"], reducer);
// yields:
Array(2) [
  0: Object {Phase: "Phase 1", count: 4, value: 50}
  1: Object {Phase: "Phase 2", count: 4, value: 130}
]

agg(tasks, ["Phase", "Step"], reducer);
// yields:
Array(4) [
  0: Object {Phase: "Phase 1", Step: "Step 1", count: 2, value: 15}
  1: Object {Phase: "Phase 1", Step: "Step 2", count: 2, value: 35}
  2: Object {Phase: "Phase 2", Step: "Step 1", count: 2, value: 55}
  3: Object {Phase: "Phase 2", Step: "Step 2", count: 2, value: 75}
]
much2learn
  • 151
  • 1
  • 2
  • 7
2

this is a TS based function, not the most performant but it's easy to read and follow!

function groupBy<T>(array: T[], key: string): Record<string, T[]> {
const groupedObject = {}
for (const item of array) {
  const value = item[key]
    if (groupedObject[value] === undefined) {
  groupedObject[value] = []
  }
  groupedObject[value].push(item)
}
  return groupedObject
}

We finish with something like ->

const data = [
{ 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" },
];
console.log(groupBy(data, 'Step'))
{
'Step 1': [
    {
      Phase: 'Phase 1',
      Step: 'Step 1',
      Task: 'Task 1',
      Value: '5'
    },
    {
      Phase: 'Phase 1',
      Step: 'Step 1',
      Task: 'Task 2',
      Value: '10'
    }
  ],
  'Step 2': [
    {
      Phase: 'Phase 1',
      Step: 'Step 2',
      Task: 'Task 1',
      Value: '15'
    },
    {
      Phase: 'Phase 1',
      Step: 'Step 2',
      Task: 'Task 2',
      Value: '20'
    }
  ]
}
1
let groupbyKeys = function(arr, ...keys) {
  let keysFieldName = keys.join();
  return arr.map(ele => {
    let keysField = {};
    keysField[keysFieldName] = keys.reduce((keyValue, key) => {
      return keyValue + ele[key]
    }, "");
    return Object.assign({}, ele, keysField);
  }).reduce((groups, ele) => {
    (groups[ele[keysFieldName]] = groups[ele[keysFieldName]] || [])
      .push([ele].map(e => {
        if (keys.length > 1) {
          delete e[keysFieldName];
        }
        return e;
    })[0]);
    return groups;
  }, {});
};

console.log(groupbyKeys(array, 'Phase'));
console.log(groupbyKeys(array, 'Phase', 'Step'));
console.log(groupbyKeys(array, 'Phase', 'Step', 'Task'));
Tom Jiang
  • 324
  • 3
  • 2
1

With sort feature

export const groupBy = function groupByArray(xs, key, sortKey) {
      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);
          el.values.sort(function(a, b) {
            return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());
          });
        } else {
          rv.push({ key: v, values: [x] });
        }

        return rv;
      }, []);
    };

Sample:

var state = [
    {
      name: "Arkansas",
      population: "2.978M",
      flag:
  "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },{
      name: "Crkansas",
      population: "2.978M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },
    {
      name: "Balifornia",
      population: "39.14M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/0/01/Flag_of_California.svg",
      category: "city"
    },
    {
      name: "Florida",
      population: "20.27M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Florida.svg",
      category: "airport"
    },
    {
      name: "Texas",
      population: "27.47M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Texas.svg",
      category: "landmark"
    }
  ];
console.log(JSON.stringify(groupBy(state,'category','name')));
amorenew
  • 10,760
  • 10
  • 47
  • 69
1

I have expanded on the accepted answer to include grouping by multiple properties, add thenby and make it purely functional with no mutation. See a demo at https://stackblitz.com/edit/typescript-ezydzv

export interface Group {
  key: any;
  items: any[];
}

export interface GroupBy {
  keys: string[];
  thenby?: GroupBy;
}

export const groupBy = (array: any[], grouping: GroupBy): Group[] => {
  const keys = grouping.keys;
  const groups = array.reduce((groups, item) => {
    const group = groups.find(g => keys.every(key => item[key] === g.key[key]));
    const data = Object.getOwnPropertyNames(item)
      .filter(prop => !keys.find(key => key === prop))
      .reduce((o, key) => ({ ...o, [key]: item[key] }), {});
    return group
      ? groups.map(g => (g === group ? { ...g, items: [...g.items, data] } : g))
      : [
          ...groups,
          {
            key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {}),
            items: [data]
          }
        ];
  }, []);
  return grouping.thenby ? groups.map(g => ({ ...g, items: groupBy(g.items, grouping.thenby) })) : groups;
};
Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
1
let x  = [
  {
    "id": "6",
    "name": "SMD L13",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  },
  {
    "id": "7",
    "name": "SMD L15",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  },
  {
    "id": "2",
    "name": "SMD L1",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  }
];

function groupBy(array, property) {
  return array.reduce((accumulator, current) => {
    const object_property = current[property];
    delete current[property]

    let classified_element = accumulator.find(x => x.id === object_property.id);
    let other_elements = accumulator.filter(x => x.id !== object_property.id);

   if (classified_element) {
     classified_element.children.push(current)
   } else {
     classified_element = {
       ...object_property, 
       'children': [current]
     }
   }
   return [classified_element, ...other_elements];
 }, [])
}

console.log( groupBy(x, 'equipmentType') )

/* output 

[
  {
    "id": "1",
    "name": "SMD",
    "children": [
      {
        "id": "6",
        "name": "SMD L13"
      },
      {
        "id": "7",
        "name": "SMD L15"
      },
      {
        "id": "2",
        "name": "SMD L1"
      }
    ]
  }
]

*/
1
function groupBy(array, groupBy){
        return array.reduce((acc,curr,index,array) => {
           var  idx = curr[groupBy]; 
              if(!acc[idx]){
                    acc[idx] = array.filter(item => item[groupBy] === idx)
              } 
            return  acc; 

        },{})
    }

// call
groupBy(items,'Step')
Ssi
  • 11
  • 2
1

Following Joseph Nields answer there's a polyfill for grouping objects in https://github.com/padcom/array-prototype-functions#arrayprototypegroupbyfieldormapper. So instead of writing that over and over again you might want to use what's already available.

Matthias Hryniszak
  • 3,099
  • 3
  • 36
  • 51
  • Your re-use of existing JS-extensions (like mentioned polyfill) seems valid for especially older JavaScript environments (not supporting groupBy from other dependencies). Maybe you could illustrate it's usage in context of this questions. Would round-up your answer :) – hc_dev Jan 24 '21 at 11:33
1

In my particular usecase, I needed to group by a property and then remove the grouping property.

That property was only added to the record for grouping purposes anyway and it wouldn't make sense for presentation to a user.

    group (arr, key) {

        let prop;

        return arr.reduce(function(rv, x) {
            prop = x[key];
            delete x[key];
            (rv[prop] = (rv[prop] || [])).push(x);
            return rv;
        }, {});

    },

Credit to @caesar-bautista for the starting function in the top answer.

Nick
  • 466
  • 1
  • 6
  • 12
1
const objsToMerge =    [ 
      { 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" }   ];

finalobj=[];
objsToMerge.forEach(e=>{
  finalobj.find(o => o.Phase === e.Phase)==null?  finalobj.push(e): (      
     finalobj[ finalobj.findIndex(instance => instance.Phase == e.Phase) ].Value= Number(finalobj[ finalobj.findIndex(instance => instance.Phase == e.Phase) ].Value)+ Number(e.Value)
  );
})
    
console.log(finalobj);

Returns as expected.

returns

    [
  { Phase: 'Phase 1', Step: 'Step 1', Task: 'Task 1', Value: 50 },
  { Phase: 'Phase 2', Step: 'Step 1', Task: 'Task 1', Value: 130 }
]
  • I had a similar problem where I was trying to coalesce an array of objects that had different group names identifying the data to be consolidated. This gave me the idea I was missing. – Ken Ingram Jul 20 '23 at 21:09
  • I didn't think of using filter to find the object and using find to get the correct index to add the data into. – Ken Ingram Jul 20 '23 at 21:20
0

I borrowed this method from underscore.js fiddler

window.helpers=(function (){
    var lookupIterator = function(value) {
        if (value == null){
            return function(value) {
                return value;
            };
        }
        if (typeof value === 'function'){
                return value;
        }
        return function(obj) {
            return obj[value];
        };
    },
    each = function(obj, iterator, context) {
        var breaker = {};
        if (obj == null) return obj;
        if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
            obj.forEach(iterator, context);
        } else if (obj.length === +obj.length) {
            for (var i = 0, length = obj.length; i < length; i++) {
                if (iterator.call(context, obj[i], i, obj) === breaker) return;
            }
        } else {
            var keys = []
            for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) keys.push(key)
            for (var i = 0, length = keys.length; i < length; i++) {
                if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
            }
        }
        return obj;
    },
    // An internal function used for aggregate "group by" operations.
    group = function(behavior) {
        return function(obj, iterator, context) {
            var result = {};
            iterator = lookupIterator(iterator);
            each(obj, function(value, index) {
                var key = iterator.call(context, value, index, obj);
                behavior(result, key, value);
            });
            return result;
        };
    };

    return {
      groupBy : group(function(result, key, value) {
        Object.prototype.hasOwnProperty.call(result, key) ? result[key].push(value) :              result[key] = [value];
        })
    };
})();

var arr=[{a:1,b:2},{a:1,b:3},{a:1,b:1},{a:1,b:2},{a:1,b:3}];
 console.dir(helpers.groupBy(arr,"b"));
 console.dir(helpers.groupBy(arr,function (el){
   return el.b>2;
 }));
Roman Yudin
  • 924
  • 10
  • 8
0
var arr = [ 
    { Phase: "Phase 1", `enter code here`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" }
];

Create and empty object. Loop through arr and add use Phase as unique key for obj. Keep updating total of key in obj while looping through arr.

const obj = {};
arr.forEach((item) => {
  obj[item.Phase] = obj[item.Phase] ? obj[item.Phase] + 
  parseInt(item.Value) : parseInt(item.Value);
});

Result will look like this:

{ "Phase 1": 50, "Phase 2": 130 }

Loop through obj to form and resultArr.

const resultArr = [];
for (item in obj) {
  resultArr.push({ Phase: item, Value: obj[item] });
}
console.log(resultArr);
0
data = [{id:1, name:'BMW'}, {id:2, name:'AN'}, {id:3, name:'BMW'}, {id:1, name:'NNN'}]
key = 'id'//try by id or name
data.reduce((previous, current)=>{
    previous[current[key]] && previous[current[key]].length != 0 ? previous[current[key]].push(current) : previous[current[key]] = new Array(current)
    return previous;
}, {})
Landaida
  • 134
  • 2
  • 8
0

Based on the original idea of @Ceasar Bautista, i modified the code and created a groupBy function using typescript.

static groupBy(data: any[], comparator: (v1: any, v2: any) => boolean, onDublicate: (uniqueRow: any, dublicateRow: any) => void) {
    return data.reduce(function (reducedRows, currentlyReducedRow) {
      let processedRow = reducedRows.find(searchedRow => comparator(searchedRow, currentlyReducedRow));

      if (processedRow) {
        // currentlyReducedRow is a dublicateRow when processedRow is not null.
        onDublicate(processedRow, currentlyReducedRow)
      } else {
        // currentlyReducedRow is unique and must be pushed in the reducedRows collection.
        reducedRows.push(currentlyReducedRow);
      }

      return reducedRows;
    }, []);
  };

This function accepts a callback (comparator) that compares the rows and finds the dublicates and a second callback (onDublicate) that aggregates the dublicates.

usage example:

data = [
    { name: 'a', value: 10 },
    { name: 'a', value: 11 },
    { name: 'a', value: 12 },
    { name: 'b', value: 20 },
    { name: 'b', value: 1 }
  ]

  private static demoComparator = (v1: any, v2: any) => {
    return v1['name'] === v2['name'];
  }

  private static demoOnDublicate = (uniqueRow, dublicateRow) => {
    uniqueRow['value'] += dublicateRow['value'];    
  };

calling

groupBy(data, demoComparator, demoOnDublicate) 

will perform a group by that calculates the sum of value.

{name: "a", value: 33}
{name: "b", value: 21}

We can create as many of these callback functions as required by the project and aggregate the values as necessary. In one case for example i needed to merge two arrays instead of summing the data.

0

You can use forEach on array and construct a new group of items. Here is how to do that with FlowType annotation

// @flow

export class Group<T> {
  tag: number
  items: Array<T>

  constructor() {
    this.items = []
  }
}

const groupBy = (items: Array<T>, map: (T) => number) => {
  const groups = []

  let currentGroup = null

  items.forEach((item) => {
    const tag = map(item)

    if (currentGroup && currentGroup.tag === tag) {
      currentGroup.items.push(item)
    } else {
      const group = new Group<T>()
      group.tag = tag
      group.items.push(item)
      groups.push(group)

      currentGroup = group
    }
  })

  return groups
}

export default groupBy

A jest test can be like

// @flow

import groupBy from './groupBy'

test('groupBy', () => {
  const items = [
    { name: 'January', month: 0 },
    { name: 'February', month: 1 },
    { name: 'February 2', month: 1 }
  ]

  const groups = groupBy(items, (item) => {
    return item.month
  })

  expect(groups.length).toBe(2)
  expect(groups[1].items[1].name).toBe('February 2')
})
onmyway133
  • 45,645
  • 31
  • 257
  • 263
0

Below function allow to groupBy (and sum values - what OP need) of arbitrary fields. In solution we define cmp function to compare two object according to grouped fields. In let w=... we create copy of subset object x fields. In y[sumBy]=+y[sumBy]+(+x[sumBy]) we use '+' to cast string to number.

function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}

const d = [ 
    { 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" }
];



function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}


// TEST
let p=(t,o) => console.log(t, JSON.stringify(o));
console.log('GROUP BY:');

p('Phase', groupBy(d,['Phase']) );
p('Step', groupBy(d,['Step']) );
p('Phase-Step', groupBy(d,['Phase', 'Step']) );
p('Phase-Task', groupBy(d,['Phase', 'Task']) );
p('Step-Task', groupBy(d,['Step', 'Task']) );
p('Phase-Step-Task', groupBy(d,['Phase','Step', 'Task']) );
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
0

just simple if you use lodash library

let temp = []
  _.map(yourCollectionData, (row) => {
    let index = _.findIndex(temp, { 'Phase': row.Phase })
    if (index > -1) {
      temp[index].Value += row.Value 
    } else {
      temp.push(row)
    }
  })
Rori
  • 1
0
var newArr = data.reduce((acc, cur) => {
    const existType = acc.find(a => a.Phase === cur.Phase);
    if (existType) {
        existType.Value += +cur.Value;
        return acc;
    }

    acc.push({
        Phase: cur.Phase,
        Value: +cur.Value
    });
    return acc;
}, []);
Lashan Fernando
  • 1,187
  • 1
  • 7
  • 21
  • Your solution is the first demonstrating both implicit requirements: (a) group-by phase & (b) aggregate-by sum over value. Could you explain the used _language concepts_ (reduce, lambdas, etc.) to share a knowledgeable receipt other than just a snippet for copy-paste? – hc_dev Jan 24 '21 at 11:39
0

A simple solution using ES6:

The method has a return model and allows the comparison of n properties.

const compareKey = (item, key, compareItem) => {
    return item[key] === compareItem[key]
}

const handleCountingRelatedItems = (listItems, modelCallback, compareKeyCallback) => {
    return listItems.reduce((previousValue, currentValue) => {
        if (Array.isArray(previousValue)) {
        const foundIndex = previousValue.findIndex(item => compareKeyCallback(item, currentValue))

        if (foundIndex > -1) {
            const count = previousValue[foundIndex].count + 1

            previousValue[foundIndex] = modelCallback(currentValue, count)

            return previousValue
        }

        return [...previousValue, modelCallback(currentValue, 1)]
        }

        if (compareKeyCallback(previousValue, currentValue)) {
        return [modelCallback(currentValue, 2)]
        }

        return [modelCallback(previousValue, 1), modelCallback(currentValue, 1)]
    })
}

const itemList = [
    { type: 'production', human_readable: 'Production' },
    { type: 'test', human_readable: 'Testing' },
    { type: 'production', human_readable: 'Production' }
]

const model = (currentParam, count) => ({
    label: currentParam.human_readable,
    type: currentParam.type,
    count
})

const compareParameter = (item, compareValue) => {
    const isTypeEqual = compareKey(item, 'type', compareValue)
    return isTypeEqual
}

const result = handleCountingRelatedItems(itemList, model, compareParameter)

 console.log('Result: \n', result)
/** Result: 
    [
        { label: 'Production', type: 'production', count: 2 },
        { label: 'Testing', type: 'testing', count: 1 }
    ]
*/
MarcosSantosDev
  • 313
  • 2
  • 5
0

In case you need to do multi-group-by:


    const populate = (entireObj, keys, item) => {
    let keysClone = [...keys],
        currentKey = keysClone.shift();

    if (keysClone.length > 0) {
        entireObj[item[currentKey]] = entireObj[item[currentKey]] || {}
        populate(entireObj[item[currentKey]], keysClone, item);
    } else {
        (entireObj[item[currentKey]] = entireObj[item[currentKey]] || []).push(item);
    }
}

export const groupBy = (list, key) => {
    return list.reduce(function (rv, x) {

        if (typeof key === 'string') (rv[x[key]] = rv[x[key]] || []).push(x);

        if (typeof key === 'object' && key.length) populate(rv, key, x);

        return rv;

    }, {});
}

const myPets = [
    {name: 'yaya', type: 'cat', color: 'gray'},
    {name: 'bingbang', type: 'cat', color: 'sliver'},
    {name: 'junior-bingbang', type: 'cat', color: 'sliver'},
    {name: 'jindou', type: 'cat', color: 'golden'},
    {name: 'dahuzi', type: 'dog', color: 'brown'},
];

// run 
groupBy(myPets, ['type', 'color']));

// you will get object like: 

const afterGroupBy = {
    "cat": {
        "gray": [
            {
                "name": "yaya",
                "type": "cat",
                "color": "gray"
            }
        ],
        "sliver": [
            {
                "name": "bingbang",
                "type": "cat",
                "color": "sliver"
            },
            {
                "name": "junior-bingbang",
                "type": "cat",
                "color": "sliver"
            }
        ],
        "golden": [
            {
                "name": "jindou",
                "type": "cat",
                "color": "golden"
            }
        ]
    },
    "dog": {
        "brown": [
            {
                "name": "dahuzi",
                "type": "dog",
                "color": "brown"
            }
        ]
    }
};

Lori Sun
  • 61
  • 6
0
var data = [{"name":1},{"name":1},{"name":2},{"name":2},{"name":3}]

groupBy(data, 'name','age','gender')


function groupBy(arr, ...keys) {
    const result = {};
    arr.forEach(obj => {
      let current = result;
      keys.forEach(key => {
        const value = obj[key];
        current[value] = current[value] || {};
        current = current[value];
      });
      current.values = current.values || [];
      current.values.push(obj);
    });
    return result;
  }

//result: 1,2,3

This code defines an array of objects called "data" with some properties. It then defines a function called "groupBy" that takes an array "arr" and a dynamic number of string arguments "keys" using the rest parameter syntax.

Inside the function, a new empty object called "result" is defined to store the final result of the groupBy function. The function then iterates over each object in the "arr" array and creates a new variable called "current" and sets it to the "result" object.

For each key in the "keys" array, the function extracts the value of the corresponding property from the current object and sets it as a new property of the "current" object. If this property does not exist, an empty object is created.

Finally, the function adds the current object to the "values" array of the "current" object.

At the end of the function, the "result" object is returned, which contains the grouped data.

megaultron
  • 399
  • 2
  • 15
0
let f_a_a_o__grouped_by_s_prop = function(
    a_o,
    s_prop
){
    let a_v = [];
    return a_o.map(
        function(o){
            let v = o[s_prop];
            if(!a_v.includes(v)){
                a_v.push(v)
                return a_o.filter(
                    o=>o[s_prop] == v
                )
            }
            return false
        }
    ).filter(v=>v);
    //test f_a_a_o__grouped_by_s_prop([{n:2},{n:2},{n:2},{n:3},{n:5},{n:5}], 'n').length == 3
}
Jonas Frey
  • 65
  • 6
-1

/**
 * array group by 
 * @category array
 * @function arrayGroupBy
 * @returns  {object} {"fieldName":[{...}],...}
 * @static
 * @author hht
 * @param {string}} key group key
 * @param {array} data array
 *
 * @example example 01 
 * --------------------------------------------------------------------------
 * import { arrayGroupBy } from "@xx/utils";
 * const array =  [
 *  {
 *    type: 'assets',
 *    name: 'zhangsan',
 *    age: '33',
 *  },
 *  {
 *    type: 'config',
 *    name: 'a',
 *    age: '13',
 *  },
 *  {
 *    type: 'run',
 *    name: 'lisi',
 *    age: '3',
 *  },
 *  {
 *    type: 'xx',
 *    name: 'timo',
 *    age: '4',
 *  },
 *];
 * arrayGroupBy(array,'type',);
 *
 * result:{
 *    assets: [{ age: '33', name: 'zhangsan', type: 'assets' }],
 *    config: [{ age: '13', name: 'a', type: 'config' }],
 *    run: [{ age: '3', name: 'lisi', type: 'run' }],
 *    xx: [{ age: '4', name: 'timo', type: 'xx' }],
 *  };
 *
 * @example example 02 null
 * --------------------------------------------------------------------------
 * const array = null;
 * arrayGroupBy(array,"type");
 *
 * result:{}
 *
 * @example example 03 key undefind
 * --------------------------------------------------------------------------
 * const array =  [
 *  {
 *    type: 'assets',
 *    name: 'zhangsan',
 *    age: '33',
 *  },
 *  {
 *    type: 'config',
 *    name: 'a',
 *    age: '13',
 *  },
 *  {
 *    type: 'run',
 *    name: 'lisi',
 *    age: '3',
 *  },
 *  {
 *    type: 'xx',
 *    name: 'timo',
 *    age: '4',
 *  },
 *];
 * arrayGroupBy(array,"xx");
 *
 * {}
 *
 */
  const arrayGroupBy = (data, key) => {
  if (!data || !Array.isArray(data)) return {};
  const groupObj = {};
  data.forEach((item) => {
    if (!item[key]) return;
    const fieldName = item[key];
    if (!groupObj[fieldName]) {
      groupObj[fieldName] = [item];
      return;
    }
    groupObj[fieldName].push(item);
  });
  return groupObj;
};

const array = [
    {
      type: 'assets',
      name: 'zhangsan',
      age: '33',
    },
    {
      type: 'config',
      name: 'a',
      age: '13',
    },
    {
      type: 'run',
      name: 'lisi',
      age: '3',
    },
    {
      type: 'run',
      name: 'wangmazi',
      age: '3',
    },
    {
      type: 'xx',
      name: 'timo',
      age: '4',
    },
  ];
console.dir(arrayGroupBy(array, 'type'))
<p>


describe('arrayGroupBy match', () => {
  const array = [
    {
      type: 'assets',
      name: 'zhangsan',
      age: '33',
    },
    {
      type: 'config',
      name: 'a',
      age: '13',
    },
    {
      type: 'run',
      name: 'lisi',
      age: '3',
    },
    {
      type: 'xx',
      name: 'timo',
      age: '4',
    },
  ];

  test('arrayGroupBy  ...', () => {
    const result = {
      assets: [{ age: '33', name: 'zhangsan', type: 'assets' }],
      config: [{ age: '13', name: 'a', type: 'config' }],
      run: [{ age: '3', name: 'lisi', type: 'run' }],
      xx: [{ age: '4', name: 'timo', type: 'xx' }],
    };

    expect(arrayGroupBy(array, 'type')).toEqual(result);
  });

  test('arrayGroupBy not match..', () => {
    // result
    expect(arrayGroupBy(array, 'xx')).toEqual({});
  });

  test('arrayGroupBy null', () => {
    let array = null;
    expect(arrayGroupBy(array, 'type')).toEqual({});
  });

  test('arrayGroupBy undefined', () => {
    let array = undefined;
    expect(arrayGroupBy(array, 'type')).toEqual({});
  });

  test('arrayGroupBy empty', () => {
    let array = [];
    expect(arrayGroupBy(array, 'type')).toEqual({});
  });
});

</p>
Game Hu
  • 11
  • 1
-2

const animals = [
  {
    type: 'dog',
    breed: 'puddle'
  },
  {
    type: 'dog',
    breed: 'labradoodle'
  },
  {
    type: 'cat',
    breed: 'siamese'
  },
  {
    type: 'dog',
    breed: 'french bulldog'
  },
  {
    type: 'cat',
    breed: 'mud'
  }
];

var groupBy = (arr, prop) =>{ 
    return arr.reduce((objs, obj) => {
        const key = obj[prop];
        if (key) {
            let fi = objs.findIndex(x => x.key == key);
            if (fi>=0) {
                objs[fi].values = [...objs[fi].values, obj];
            } else {
                objs.push({
                    key: key,
                    values: [obj]
                })
            }
        }
        return objs;
    }, []);
}

console.log(groupBy(animals, 'type'))
  • 3
    Do not post an answer with merely codes. While your solution can be useful, you should also explain why the code will fix the problem that was described in the question. – 4b0 Dec 18 '22 at 08:41