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?
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"]}
Using ES6 Map object:
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"}]
About Map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
with ES6:
const groupBy = (items, key) => items.reduce(
(result, item) => ({
...result,
[item[key]]: [
...(result[item[key]] || []),
item,
],
}),
{},
);
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!
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
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();
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:
_.groupBy()
)Map
rather than an object (e.g. as returned by _.groupBy()
). This has lots of benefits, including:
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).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()
);
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}
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]
};
});
};
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:
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 }]
// }
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; }
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"]}
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 ;)
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"}]
}
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.
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.
Checked answer -- is not grouping solve, however is a direct answer.
REAL GROUP BY for Array of Objects by some field with calculated key name.
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
Also you can groups data recursively. For example initially group by Phase
, then by Step
field.
Verify it by yourself, just run it. ??? ?? ????? ???, ??? ???? ???????? ?????????????
Wish you be a successful.
?? ??????????? ??????? ?????????? ?????????? ????????????? ?? ??? ??????????? ????? ????????????! ???, ????????!
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.
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
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);
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...
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>
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.
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'));
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]
}), {})
}
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;
}());
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')));
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.
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);
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);
Here's a nasty, hard to read solution using ES6:
export default (array, key) => {
return array.reduce(
(r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
{}
);
};
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)
//}
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')
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;
};
I would check declarative-js groupBy
it seems to do exactly what you are looking for. It is also:
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());
©2020 All rights reserved.