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?
This question is related to
javascript
arrays
object
group-by
underscore.js
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.
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"}]
}
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 ES6:
const groupBy = (items, key) => items.reduce(
(result, item) => ({
...result,
[item[key]]: [
...(result[item[key]] || []),
item,
],
}),
{},
);
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.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 }]
_x000D_
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.
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);
_x000D_
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 ) );
_x000D_
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;
};
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;
}, []);
/**
* 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'))
_x000D_
<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>
_x000D_
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)
//}
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());
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[]>);
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.
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]
}), {})
}
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);
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')
})
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:
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:
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
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();
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;
}, {})
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')
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()
);
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"
}
]
}
]
*/
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 }
]
*/
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}
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 ;)
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')));
Array.prototype.groupBy = function (groupingKeyFn) {_x000D_
if (typeof groupingKeyFn !== 'function') {_x000D_
throw new Error("groupBy take a function as only parameter");_x000D_
}_x000D_
return this.reduce((result, item) => {_x000D_
let key = groupingKeyFn(item);_x000D_
if (!result[key])_x000D_
result[key] = [];_x000D_
result[key].push(item);_x000D_
return result;_x000D_
}, {});_x000D_
}_x000D_
_x000D_
var a = [_x000D_
{type: "video", name: "a"},_x000D_
{type: "image", name: "b"},_x000D_
{type: "video", name: "c"},_x000D_
{type: "blog", name: "d"},_x000D_
{type: "video", name: "e"},_x000D_
]_x000D_
console.log(a.groupBy((item) => item.type));
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
_x000D_
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.
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}]_x000D_
_x000D_
const group = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});_x000D_
_x000D_
const groupBy = (arr, k, fn = () => true) => _x000D_
arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});_x000D_
_x000D_
console.log(group(data, 'id')) // grouping via `reduce`_x000D_
console.log(groupBy(data, 'id')) // same result if `fn` is omitted_x000D_
console.log(groupBy(data, 'score', x => x > 2 )) // group with the iteratee
_x000D_
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" } ]_x000D_
_x000D_
const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});_x000D_
const groupWith = (arr, k, fn = () => true) => _x000D_
arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});_x000D_
_x000D_
console.log(groupBy(data, 'Phase'))_x000D_
console.log(groupWith(data, 'Value', x => x > 30 )) // group by `Value` > 30
_x000D_
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"}]_x000D_
_x000D_
const groupKeys = arr => _x000D_
arr.reduce((r,c) => (Object.keys(c).map(x => r[c[x]] = [...r[c[x]] || [], x]),r),{});_x000D_
_x000D_
console.log(groupKeys(data))
_x000D_
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.
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
const groupBy = (x,f)=>x.reduce((a,b)=>((a[f(b)]||=[]).push(b),a),{});
EXAMPLES
const groupBy = (x, f) => x.reduce((a, b) => ((a[f(b)] ||= []).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 },
// ],
// "2-1": [
// { comment: "klm", forItem: 1, inModule: 2 },
// { comment: "xyz", forItem: 1, inModule: 2 },
// ],
// }
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 = [ _x000D_
{ Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },_x000D_
{ Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },_x000D_
{ Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },_x000D_
{ Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },_x000D_
{ Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },_x000D_
{ Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },_x000D_
{ Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },_x000D_
{ Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }_x000D_
];_x000D_
_x000D_
_x000D_
_x000D_
function groupBy(data, fields, sumBy='Value') {_x000D_
let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);_x000D_
data.forEach(x=> {_x000D_
let y=r.find(z=>cmp(x,z));_x000D_
let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})_x000D_
y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);_x000D_
});_x000D_
return r;_x000D_
}_x000D_
_x000D_
_x000D_
// TEST_x000D_
let p=(t,o) => console.log(t, JSON.stringify(o));_x000D_
console.log('GROUP BY:');_x000D_
_x000D_
p('Phase', groupBy(d,['Phase']) );_x000D_
p('Step', groupBy(d,['Step']) );_x000D_
p('Phase-Step', groupBy(d,['Phase', 'Step']) );_x000D_
p('Phase-Task', groupBy(d,['Phase', 'Task']) );_x000D_
p('Step-Task', groupBy(d,['Step', 'Task']) );_x000D_
p('Phase-Step-Task', groupBy(d,['Phase','Step', 'Task']) );
_x000D_
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)
}
})
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);
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){_x000D_
return function iterate([x,...xs], i = 0, r = [[],[]]){_x000D_
cb(x,i,[x,...xs]) ? (r[0].push(x), r)_x000D_
: (r[1].push(x), r);_x000D_
return xs.length ? iterate(xs, ++i, r) : r;_x000D_
}(this);_x000D_
};_x000D_
_x000D_
var arr = [0,1,2,3,4,5,6,7,8,9],_x000D_
res = arr.groupBy(e => e < 5);_x000D_
console.log(res);
_x000D_
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!
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}
]
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...
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;
}));
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]
};
});
};
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 }]
// }
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]
_x000D_
About Map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
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) {_x000D_
var map = new Map;_x000D_
groups = [].concat(groups);_x000D_
return array.reduce((r, o) => {_x000D_
groups.reduce((m, k, i, { length }) => {_x000D_
var child;_x000D_
if (m.has(o[k])) return m.get(o[k]);_x000D_
if (i + 1 === length) {_x000D_
child = Object_x000D_
.assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 });_x000D_
r.push(child);_x000D_
} else {_x000D_
child = new Map;_x000D_
}_x000D_
m.set(o[k], child);_x000D_
return child;_x000D_
}, map)[valueKey] += +o[valueKey];_x000D_
return r;_x000D_
}, [])_x000D_
};_x000D_
_x000D_
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" }];_x000D_
_x000D_
console.log(groupBy(data, 'Phase', 'Value'));_x000D_
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
_x000D_
.as-console-wrapper { max-height: 100% !important; top: 0; }
_x000D_
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.
Source: Stackoverflow.com