admin管理员组文章数量:1134247
Grouping elements in array by multiple properties is the closest match to my question as it indeed groups objects by multiple keys in an array. Problem is this solution doesn't sum up the properties value then remove the duplicates, it instead nests all the duplicates in a two-dimensional arrays.
Expected behavior
I have an array of objects which must be grouped by shape
and color
.
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
Objects in this array are considered duplicates only if both their shape
and color
are the same. If they are, I want to respectively sum up their used
and instances
values then delete the duplicates.
So in this example result array may only contain four combinations : square red
, square blue
, circle red
, circle blue
Problem
I tried a simpler approach here:
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'red', used: 4, instances: 4},
{shape: 'square', color: 'red', used: 2, instances: 2}
];
result = [];
arr.forEach(function (a) {
if ( !this[a.color] && !this[a.shape] ) {
this[a.color] = { color: a.color, shape: a.shape, used: 0, instances: 0 };
result.push(this[a.color]);
}
this[a.color].used += a.used;
this[a.color].instances += a.instances;
}, Object.create(null));
console.log(result);
Grouping elements in array by multiple properties is the closest match to my question as it indeed groups objects by multiple keys in an array. Problem is this solution doesn't sum up the properties value then remove the duplicates, it instead nests all the duplicates in a two-dimensional arrays.
Expected behavior
I have an array of objects which must be grouped by shape
and color
.
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
Objects in this array are considered duplicates only if both their shape
and color
are the same. If they are, I want to respectively sum up their used
and instances
values then delete the duplicates.
So in this example result array may only contain four combinations : square red
, square blue
, circle red
, circle blue
Problem
I tried a simpler approach here:
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'red', used: 4, instances: 4},
{shape: 'square', color: 'red', used: 2, instances: 2}
];
result = [];
arr.forEach(function (a) {
if ( !this[a.color] && !this[a.shape] ) {
this[a.color] = { color: a.color, shape: a.shape, used: 0, instances: 0 };
result.push(this[a.color]);
}
this[a.color].used += a.used;
this[a.color].instances += a.instances;
}, Object.create(null));
console.log(result);
but it outputs
[{shape: "square", color: "red", used: 11, instances: 9},
{shape: "circle", color: "blue", used: 4, instances: 4}]
instead of expected result:
[{shape: "square", color: "red", used: 5, instances: 3},
{shape: "circle", color: "red", used: 2, instances: 1},
{shape: "square", color: "blue", used: 11, instances: 9},
{shape: "circle", color: "blue", used: 0, instances: 0}]
How can I get my function to properly group the objects by shape and color ? i.e. sum up their values and remove the duplicates ?
Share Improve this question edited Oct 17, 2017 at 15:58 Barmar 780k56 gold badges542 silver badges656 bronze badges asked Oct 17, 2017 at 15:51 Hal_9100Hal_9100 9511 gold badge9 silver badges20 bronze badges 3 |18 Answers
Reset to default 100Use Array#reduce with a helper object to group similar objects. For each object, check if the combined shape
and color
exists in the helper. If it doesn't, add to the helper using Object#assign to create a copy of the object, and push to the array. If it does, add it's values to used
and instances
.
var arr = [{"shape":"square","color":"red","used":1,"instances":1},{"shape":"square","color":"red","used":2,"instances":1},{"shape":"circle","color":"blue","used":0,"instances":0},{"shape":"square","color":"blue","used":4,"instances":4},{"shape":"circle","color":"red","used":1,"instances":1},{"shape":"circle","color":"red","used":1,"instances":0},{"shape":"square","color":"blue","used":4,"instances":5},{"shape":"square","color":"red","used":2,"instances":1}];
var helper = {};
var result = arr.reduce(function(r, o) {
var key = o.shape + '-' + o.color;
if(!helper[key]) {
helper[key] = Object.assign({}, o); // create a copy of o
r.push(helper[key]);
} else {
helper[key].used += o.used;
helper[key].instances += o.instances;
}
return r;
}, []);
console.log(result);
If you can use ES6, you use a Map to collect the values, and then convert it back to an array by spreading the Map#values:
const arr = [{"shape":"square","color":"red","used":1,"instances":1},{"shape":"square","color":"red","used":2,"instances":1},{"shape":"circle","color":"blue","used":0,"instances":0},{"shape":"square","color":"blue","used":4,"instances":4},{"shape":"circle","color":"red","used":1,"instances":1},{"shape":"circle","color":"red","used":1,"instances":0},{"shape":"square","color":"blue","used":4,"instances":5},{"shape":"square","color":"red","used":2,"instances":1}];
const result = [...arr.reduce((r, o) => {
const key = o.shape + '-' + o.color;
const item = r.get(key) || Object.assign({}, o, {
used: 0,
instances: 0
});
item.used += o.used;
item.instances += o.instances;
return r.set(key, item);
}, new Map).values()];
console.log(result);
Use this method to specify multiple properties:
public static groupBy(array, f) {
let groups = {};
array.forEach(function (o) {
var group = JSON.stringify(f(o));
groups[group] = groups[group] || [];
groups[group].push(o);
});
return Object.keys(groups).map(function (group) {
return groups[group];
})
}
Call this method like:
var result = Utils.groupBy(arr, function (item) {
return [item.shape, item.color];
});
You can use reduce()
to create one object of unique shape|color
properties and Object.values()
to return array of those values.
var arr =[{"shape":"square","color":"red","used":1,"instances":1},{"shape":"square","color":"red","used":2,"instances":1},{"shape":"circle","color":"blue","used":0,"instances":0},{"shape":"square","color":"blue","used":4,"instances":4},{"shape":"circle","color":"red","used":1,"instances":1},{"shape":"circle","color":"red","used":1,"instances":0},{"shape":"square","color":"blue","used":4,"instances":5},{"shape":"square","color":"red","used":2,"instances":1}]
var result = Object.values(arr.reduce(function(r, e) {
var key = e.shape + '|' + e.color;
if (!r[key]) r[key] = e;
else {
r[key].used += e.used;
r[key].instances += e.instances
}
return r;
}, {}))
console.log(result)
Here is a more general grouping and summing function that accepts an array of objects, an array of keys to group by, and an array of keys to sum.
function groupAndSum(arr, groupKeys, sumKeys){
return Object.values(
arr.reduce((acc,curr)=>{
const group = groupKeys.map(k => curr[k]).join('-');
acc[group] = acc[group] || Object.fromEntries(
groupKeys.map(k => [k, curr[k]]).concat(sumKeys.map(k => [k, 0])));
sumKeys.forEach(k => acc[group][k] += curr[k]);
return acc;
}, {})
);
}
Demo:
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
function groupAndSum(arr, groupKeys, sumKeys){
return Object.values(
arr.reduce((acc,curr)=>{
const group = groupKeys.map(k => curr[k]).join('-');
acc[group] = acc[group] || Object.fromEntries(groupKeys.map(k => [k, curr[k]]).concat(sumKeys.map(k => [k, 0])));
sumKeys.forEach(k => acc[group][k] += curr[k]);
return acc;
}, {})
);
}
const res = groupAndSum(arr, ['shape', 'color'], ['used', 'instances']);
console.log(res);
You could use a hash table and the keys for grouping same groups.
var array = [{ shape: 'square', color: 'red', used: 1, instances: 1 }, { shape: 'square', color: 'red', used: 2, instances: 1 }, { shape: 'circle', color: 'blue', used: 0, instances: 0 }, { shape: 'square', color: 'blue', used: 4, instances: 4 }, { shape: 'circle', color: 'red', used: 1, instances: 1 }, { shape: 'circle', color: 'red', used: 1, instances: 0 }, { shape: 'square', color: 'blue', used: 4, instances: 5 }, { shape: 'square', color: 'red', used: 2, instances: 1 }],
hash = Object.create(null),
grouped = [];
array.forEach(function (o) {
var key = ['shape', 'color'].map(function (k) { return o[k]; }).join('|');
if (!hash[key]) {
hash[key] = { shape: o.shape, color: o.color, used: 0, instances: 0 };
grouped.push(hash[key]);
}
['used', 'instances'].forEach(function (k) { hash[key][k] += o[k]; });
});
console.log(grouped);
.as-console-wrapper { max-height: 100% !important; top: 0; }
ES6 answers as requisted by user:
// To call this function:
// const result = this.toolBox.multipleGroupByArray(
// dataArray, (property: IProperty) => [property.prop1, property.prop2, property.prop3]);
multipleGroupByArray(dataArray, groupPropertyArray) {
const groups = {};
dataArray.forEach(item => {
const group = JSON.stringify(groupPropertyArray(item));
groups[group] = groups[group] || [];
groups[group].push(item);
});
return Object.keys(groups).map(function(group) {
return groups[group];
});
}
In case if you need an array of 'used' and 'instances' based on the colour or shape property; then you can use this code .
(PS : I know this is not what you are looking for but in future it may help someone. Also am reusing Nenand's code for this purpose . If the code is useful to you just thank him )
var array = [{ shape: 'square', color: 'red', used: 1, instances: 1 }, { shape: 'square', color: 'red', used: 2, instances: 1 }, { shape: 'circle', color: 'blue', used: 0, instances: 0 }, { shape: 'square', color: 'blue', used: 4, instances: 4 }, { shape: 'circle', color: 'red', used: 1, instances: 1 }, { shape: 'circle', color: 'red', used: 1, instances: 0 }, { shape: 'square', color: 'blue', used: 4, instances: 5 }, { shape: 'square', color: 'red', used: 2, instances: 1 }],
hash = Object.create(null),
grouped = [];
array.forEach(function (o) {
var key = ['shape', 'color'].map(function (k) { return o[k]; }).join('|');
if (!hash[key]) {
hash[key] = { shape: o.shape, color: o.color, YourArrayName : [] };
grouped.push(hash[key]);
}
['used'].forEach(function (k) { hash[key]['YourArrayName'].push({ used : o['used'], instances : o['instances'] }) });
});
console.log(grouped);
The output will be in like
If you want groupBy keys as per conditional fields then here is the modification for @Abbes answer:
function groupBy(array, f) {
let groups = {};
array.forEach((o) => {
var group = f(o).join('-');
groups[group] = groups[group] || [];
groups[group].push(o);
});
return groups;
}
And use it like:
groupBy(connectedServers, (item) => {
return [item.key1, item.key2];
});
/**
* Groups an array of objects with multiple properties.
*
* @param {Array} array: the array of objects to group
* @param {Array} props: the properties to groupby
* @return {Array} an array of arrays with the grouped results
*/
const groupBy = ({ Group: array, By: props }) => {
getGroupedItems = (item) => {
returnArray = [];
let i;
for (i = 0; i < props.length; i++) {
returnArray.push(item[props[i]]);
}
return returnArray;
};
let groups = {};
let i;
for (i = 0; i < array.length; i++) {
const arrayRecord = array[i];
const group = JSON.stringify(getGroupedItems(arrayRecord));
groups[group] = groups[group] || [];
groups[group].push(arrayRecord);
}
return Object.keys(groups).map((group) => {
return groups[group];
});
};
Example:
Assume that we have an array of objects. Each object contains info about a person and the money that possess. We want to sum up the money for all persons with the same Nationality and with the same gender.
const data = [
{Name: 'George', Surname: 'Best', Country: 'Great Britain', Gender: 'Male', Money:8000},
{Name: 'Orion', Surname: 'Papathanasiou', Country: 'Greece', Gender: 'Male', Money: 2000},
{Name: 'Mairy', Surname: 'Wellbeck', Country: 'Great Britain', Gender: 'Female', Money:5000},
{Name: 'Thanasis', Surname: 'Papathanasiou', Country: 'Greece',Gender: 'Male', Money: 3200},
{Name: 'George', Surname: 'Washington', Country: 'Great Britain', Gender: 'Male',Money:4200},
{Name: 'Orfeas', Surname: 'Kalaitzis', Country: 'Greece', Gender: 'Male', Money: 7643},
{Name: 'Nick', Surname: 'Wellington', Country: 'USA', Gender: 'Male', Money:1000},
{Name: 'Kostas', Surname: 'Antoniou', Country: 'Greece', Gender: 'Male', Money: 8712},
{Name: 'John', Surname: 'Oneal', Country: 'USA', Gender: 'Male', Money:98234},
{Name: 'Paulos', Surname: 'Stamou', Country: 'Greece', Gender: 'Male', Money: 3422},
{Name: 'Soula', Surname: 'Spuropoulou', Country: 'Greece', Gender: 'Female', Money:400},
{Name: 'Paul', Surname: 'Pierce', Country: 'USA', Gender: 'Male',Money: 13000},
{Name: 'Helen', Surname: 'Smith', Country: 'Great Britain', Gender: 'Female', Money:1000},
{Name: 'Cathrine', Surname: 'Bryant', Country: 'Great Britain', Gender: 'Female', Money: 8712},
{Name: 'Jenny', Surname: 'Scalabrini', Country: 'USA', Gender: 'Female', Money:92214}];
const groupByProperties = ['Country', 'Gender'];
Calling the function:
const groupResult = groupBy( {Group: data, By: groupByProperties} );
The group result is:
(6) [Array(2), Array(5), Array(3), Array(3), Array(1), Array(1)]
0: Array(2)
0: {Name: "George", Surname: "Best", Country: "Great Britain", Gender: "Male", Money: 8000}
1: {Name: "George", Surname: "Washington", Country: "Great Britain", Gender: "Male", Money: 4200}
length: 2
__proto__: Array(0)
1: Array(5)
0: {Name: "Orion", Surname: "Papathanasiou", Country: "Greece", Gender: "Male", Money: 2000}
1: {Name: "Thanasis", Surname: "Papathanasiou", Country: "Greece", Gender: "Male", Money: 3200}
2: {Name: "Orfeas", Surname: "Kalaitzis", Country: "Greece", Gender: "Male", Money: 7643}
3: {Name: "Kostas", Surname: "Antoniou", Country: "Greece", Gender: "Male", Money: 8712}
4: {Name: "Paulos", Surname: "Stamou", Country: "Greece", Gender: "Male", Money: 3422}
length: 5
__proto__: Array(0)
2: Array(3)
0: {Name: "Mairy", Surname: "Wellbeck", Country: "Great Britain", Gender: "Female", Money: 5000}
1: {Name: "Helen", Surname: "Smith", Country: "Great Britain", Gender: "Female", Money: 1000}
2: {Name: "Cathrine", Surname: "Bryant", Country: "Great Britain", Gender: "Female", Money: 8712}
length: 3
__proto__: Array(0)
3: Array(3)
0: {Name: "Nick", Surname: "Wellington", Country: "USA", Gender: "Male", Money: 1000}
1: {Name: "John", Surname: "Oneal", Country: "USA", Gender: "Male", Money: 98234}
2: {Name: "Paul", Surname: "Pierce", Country: "USA", Gender: "Male", Money: 13000}
length: 3
__proto__: Array(0)
4: Array(1)
0: {Name: "Soula", Surname: "Spuropoulou", Country: "Greece", Gender: "Female", Money: 400}
length: 1
__proto__: Array(0)
5: Array(1)
0: {Name: "Jenny", Surname: "Scalabrini", Country: "USA", Gender: "Female", Money: 92214}
length: 1
__proto__: Array(0)
length: 6
__proto__: Array(0)
So, we got 6 arrays. Each array is grouped by Country
and by Gender
Iterating over each array, we can sum up the money!
const groupBy = ({ Group: array, By: props }) => {
getGroupedItems = (item) => {
returnArray = [];
let i;
for (i = 0; i < props.length; i++) {
returnArray.push(item[props[i]]);
}
return returnArray;
};
let groups = {};
let i;
for (i = 0; i < array.length; i++) {
const arrayRecord = array[i];
const group = JSON.stringify(getGroupedItems(arrayRecord));
groups[group] = groups[group] || [];
groups[group].push(arrayRecord);
}
return Object.keys(groups).map((group) => {
return groups[group];
});
};
const data = [
{Name: 'George', Surname: 'Best', Country: 'Great Britain', Gender: 'Male', Money:8000},
{Name: 'Orion', Surname: 'Papathanasiou', Country: 'Greece', Gender: 'Male', Money: 2000},
{Name: 'Mairy', Surname: 'Wellbeck', Country: 'Great Britain', Gender: 'Female', Money:5000},
{Name: 'Thanasis', Surname: 'Papathanasiou', Country: 'Greece',Gender: 'Male', Money: 3200},
{Name: 'George', Surname: 'Washington', Country: 'Great Britain', Gender: 'Male',Money:4200},
{Name: 'Orfeas', Surname: 'Kalaitzis', Country: 'Greece', Gender: 'Male', Money: 7643},
{Name: 'Nick', Surname: 'Wellington', Country: 'USA', Gender: 'Male', Money:1000},
{Name: 'Kostas', Surname: 'Antoniou', Country: 'Greece', Gender: 'Male', Money: 8712},
{Name: 'John', Surname: 'Oneal', Country: 'USA', Gender: 'Male', Money:98234},
{Name: 'Paulos', Surname: 'Stamou', Country: 'Greece', Gender: 'Male', Money: 3422},
{Name: 'Soula', Surname: 'Spuropoulou', Country: 'Greece', Gender: 'Female', Money:400},
{Name: 'Paul', Surname: 'Pierce', Country: 'USA', Gender: 'Male',Money: 13000},
{Name: 'Helen', Surname: 'Smith', Country: 'Great Britain', Gender: 'Female', Money:1000},
{Name: 'Cathrine', Surname: 'Bryant', Country: 'Great Britain', Gender: 'Female', Money: 8712},
{Name: 'Jenny', Surname: 'Scalabrini', Country: 'USA', Gender: 'Female', Money:92214}];
const groupByProperties = ['Country', 'Gender'];
const groupResult = groupBy( {Group: data, By: groupByProperties} );
console.log(groupResult);
I have a suggestion for you. If you want to make it easier to do, you try the Underscore library : http://underscorejs.org/
I tried quickly to use it and got the right result :
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
var byshape = _.groupBy(arr, 'shape');
var bycolor = _.map(byshape, function(array) {
return _.groupBy(array, 'color')
});
var output = [];
_.each(bycolor, function(arrayOfShape) {
_.each(arrayOfShape, function(arrayOfColor) {
var computedItem = {shape: "", color: "", used: 0, instances: 0};
_.each(arrayOfColor, function(item) {
computedItem.shape = item.shape;
computedItem.color = item.color;
computedItem.used += item.used;
computedItem.instances += item.instances;
});
output.push(computedItem);
});
});
console.log(output);
http://jsfiddle.net/oLyzdoo7/
This solution groups first data, then you can do what you want after, for example, compute data as ypur wish.
Maybe you can optimize it, let me know if you need more help
I found some of these answers a little hard to reuse so here is a reusable function that you can pass in what keys you want your grouping to use.
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
const groupByMultipleKeys = (items, keys) =>
items.reduce((acc, item) => {
const isExistingItem = acc
.flatMap(accItem => accItem)
.find(accItem =>
keys.every(key => accItem[key] === item[key])
)
if (isExistingItem) {
return acc;
}
const allRelatedItems = items.filter(ungroupedItem =>
keys.every(key => ungroupedItem[key] === item[key])
)
acc.push(allRelatedItems)
return acc
}, [])
const groupedItem = groupByMultipleKeys(arr, ['shape', 'color'])
console.log('groupedItem', groupedItem)
- List item
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
result = [];
arr.forEach(function (a) {
if ( !this[a.color] && !this[a.shape] ) {
this[a.color] = { color: a.color, shape: a.shape, used: 0, instances: 0 };
result.push(this[a.color]);
}
this[a.color].used += a.used;
this[a.color].instances += a.instances;
}, Object.create(null));
console.log(result);
**Output:**
[
{
"color": "red",
"shape": "square",
"used": 11,
"instances": 9
},
{
"color": "blue",
"shape": "circle",
"used": 4,
"instances": 4
}
]
thats all perfetcly working.
Enjoy your coding....
1.sumkeys 3.groupkeys
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'red', used: 4, instances: 4},
{shape: 'square', color: 'red', used: 2, instances: 2}
];
function groupbykeys(arr, groupKeys, sumKeys){
var hash = Object.create(null),
grouped = [];
arr.forEach(function (o) {
var key = groupKeys.map(function (k) { return o[k]; }).join('|');
if (!hash[key]) {
hash[key] = Object.keys(o).reduce((result, key)=> {
result[key]=o[key];
if(sumKeys.includes(key))
result[key]=0;
return result;
}, { }); //map_(o) //{ shape: o.shape, color: o.color, used: 0, instances: 0 };
grouped.push(hash[key]);
}
sumKeys.forEach(function (k) { hash[key][k] += o[k]; });
});
return grouped;
}
var result=groupbykeys(arr,['shape','color'],['used','instances']);
console.log(result)
Aggregate into an object keyed by the unique combination of shape and color with reduce then take the values out of it:
const aggregate = xs => Object.values(
xs.reduce((acc, {shape, color, used, instances}) => {
const key = shape + color;
acc[key] ??= {shape, color, used: 0, instances: 0};
acc[key].used += used;
acc[key].instances += instances;
return acc;
}, {})
);
console.log(aggregate(arr));
<script>
const arr =
[ {shape: 'square', color: 'red', used: 1, instances: 1}
, {shape: 'square', color: 'red', used: 2, instances: 1}
, {shape: 'circle', color: 'blue', used: 0, instances: 0}
, {shape: 'square', color: 'blue', used: 4, instances: 4}
, {shape: 'circle', color: 'red', used: 1, instances: 1}
, {shape: 'circle', color: 'red', used: 1, instances: 0}
, {shape: 'square', color: 'blue', used: 4, instances: 5}
, {shape: 'square', color: 'red', used: 2, instances: 1}];
</script>
Much more compact ES6 version, which uses JSON.stringify to ensure proper separation between the properties that are being grouped by.
const arr = [{shape: 'square', color: 'red', used: 1, instances: 1}, {shape: 'square', color: 'red', used: 2, instances: 1}, {shape: 'circle', color: 'blue', used: 0, instances: 0}, {shape: 'square', color: 'blue', used: 4, instances: 4}, {shape: 'circle', color: 'red', used: 1, instances: 1}, {shape: 'circle', color: 'red', used: 1, instances: 0}, {shape: 'square', color: 'blue', used: 4, instances: 5}, {shape: 'square', color: 'red', used: 2, instances: 1}];
let grouped = Object.values(arr.reduce((a,c)=> {
let i = a[JSON.stringify([c.shape, c.color])] ??= {...c, used: 0, instances: 0};
i.used += c.used; i.instances += c.instances; return a;
} , {}));
console.log(grouped);
Or, to make it easier to specify arbitrary grouping properties and summing properties:
const arr = [{shape: 'square', color: 'red', used: 1, instances: 1}, {shape: 'square', color: 'red', used: 2, instances: 1}, {shape: 'circle', color: 'blue', used: 0, instances: 0}, {shape: 'square', color: 'blue', used: 4, instances: 4}, {shape: 'circle', color: 'red', used: 1, instances: 1}, {shape: 'circle', color: 'red', used: 1, instances: 0}, {shape: 'square', color: 'blue', used: 4, instances: 5}, {shape: 'square', color: 'red', used: 2, instances: 1}];
function groupAndSum(arr, groupProps, sumProps) {
return Object.values(arr.reduce((a,c)=> {
let i = a[JSON.stringify(groupProps.map(p=>[p,c[p]]))] ??=
{...c, ...Object.fromEntries(sumProps.map(p=>[p, 0]))};
sumProps.forEach(p=>i[p]+=c[p]); return a;
} , {}));
}
console.log(groupAndSum(arr, ['shape', 'color'], ['used', 'instances']));
const v = [{"shape":"square","color":"red","used":1,"instances":1},{"shape":"square","color":"red","used":2,"instances":1},{"shape":"circle","color":"blue","used":0,"instances":0},{"shape":"square","color":"blue","used":4,"instances":4},{"shape":"circle","color":"red","used":1,"instances":1},{"shape":"circle","color":"red","used":1,"instances":0},{"shape":"square","color":"blue","used":4,"instances":5},{"shape":"square","color":"red","used":2,"instances":1}];
function groupBy<T>(
arr: T[],
vInitial: Partial<T>,
fn: (curr: T, acc: T) => void,
...args: Array<keyof T>
) {
return Array.from(
arr
.reduce((r, o) => {
const key = args.map((k) => o[k]).join("|");
const ob = Object.assign({}, o);
const obj = {};
for (const key of Object.keys(ob)) {
if (
vInitial != null &&
vInitial.hasOwnProperty(key) &&
vInitial[key] != null
) {
obj[key] = vInitial[key];
}
}
const item = r.get(key) ?? Object.assign({}, o, obj);
fn(item, o);
return r.set(key, item);
}, new Map<string, T>())
.values(),
);
}
console.log(groupBy(
v,
{},
(item, o) => {
item.used += o.used;
item.instances += o.instances;
},
'shape', 'color'));
This is an extremely old post, but I wanted to share my solution here. It's very similar to the others posted, but I wanted to expand the notes and comments to help others understand the algorithm
I write in typescript, but it can be converted to vanilla JS rather easily.
The concept of grouping can only be achieved using simple data types (string
, number
, or boolean
) and we can use Date
since it can be reduced to a number
using the Date.getTime
I define these types in a Typescript type
type SimpleValue = string | number | boolean | Date;
Then, we need to capture the properties of an object which are a SimpleValue
.
KeysMatching
will filter an object at the first level by any keys which match the SimpleValue
data type
type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];
They are then enumerated in SimpleProps
type SimpleProps<T extends object> = KeysMatching<T, SimpleValue>;
We define the groupBy
function which will aggregate the array. Sometimes the key we want to group on doesn't exist on the row
itself and has to be computed, so we accept either a simple property or a function which returns a SimpleValue
.
The initialValue
is used the first time a unique key is returned to get the "genesis" summary or initial summary object.
The reducer
will be used to aggregate the row
against the summary
generated
/**
* Reduces a complex array into an array of summary values
*
* *NOTE: For reducing an array to a SINGLE value, use the standard Array.reduce method*
* @param arr The initial array list
* @param groupKey A key from the row *OR* a function to resolve a unique key from the row - if the return value is undefined, a key will be generated
* @param initialValue The initial summary value, fired the first time a unique value is found
* @param reducer A function to summarize against the initial value
* @returns A distinct array of summarized values
*/
function groupBy<T extends object, U>(
arr: T[],
groupKey: SimpleProps<T> | ((row: T, index?: number) => SimpleValue),
initialValue: (row: T) => U,
reducer: (previousValue: U, currentValue: T) => U | void) {
// stores the current summary values by key
const map: Record<string, U> = {};
// helps us resolve the key from either a function or property of the original row
function resolveKey(row: T, index?: number) : string {
const key: any = typeof groupKey === "function" ? groupKey(row, index) : row[groupKey];
// DO NOT PUT `===` here, we want to capture any undefined OR null values
if (key == null) {
return `__unmapped${index}`;
}
// If the type is "Date", we just get the "time" component
if (key instanceof Date) {
return `${key.getTime()}`;
}
// cast the key to a string (you can also use the `toString` method here
return `${key}`;
}
// iterate over the array
arr.forEach((v, i) => {
// compute the unique key for the given row
const key = resolveKey(v, i);
// get the existing summary object (if it exists)
const summary = map[key];
if (summary) {
// compute the new summary from the original summary and the current row - if the "reducer" returns null or undefined, we default back to the original summary (this is to support summary objects which don't need to be continuously recreated)
map[key] = reducer(summary, v) || summary;
} else {
// generate a new summary value based on the current row
map[key] = initialValue(v);
}
});
// get the values from the summary map
return Object
.values(map);
}
For the above example, the implementation would look something like this
// we want to group by "shape" AND "color"
var arr = [
{shape: 'square', color: 'red', used: 1, instances: 1},
{shape: 'square', color: 'red', used: 2, instances: 1},
{shape: 'circle', color: 'blue', used: 0, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 4},
{shape: 'circle', color: 'red', used: 1, instances: 1},
{shape: 'circle', color: 'red', used: 1, instances: 0},
{shape: 'square', color: 'blue', used: 4, instances: 5},
{shape: 'square', color: 'red', used: 2, instances: 1}
];
const results = groupBy(arr,
// compute the key, we need to use a function here since we are aggregating on two fields instead of one
row => `${row.shape}_${row.color}`,
// In this case, the initial value is the first record with the unique key, but we could easily generate a new object type if needed
// for the sake of object immutability, I create a copy of the original row using the "spread" operator, but if this isn't important to you, you can just return the original row
row => ({
...row
}),
// We perform our aggregations
(previousSummary, currentRow) => {
previousSummary.used += currentRow.used;
previousSummary.instances += currentRow.instances;
// we don't need to return anything here as the "groupBy" function is already defaulting back to the current summary object
});
// output the results
console.log(results);
Typescript version of previous answer by stretch0: https://stackoverflow.com/a/73787810/7303134
const groupByMultipleKeys = <T>(items: T[], keys: (keyof T)[]) => {
return items.reduce((acc, item) => {
const isExistingItem = acc
.flatMap((accItem) => accItem)
.find((accItem) => keys.every((key) => accItem[key] === item[key]))
if (isExistingItem) {
return acc
}
const allRelatedItems = items.filter((ungroupedItem) =>
keys.every((key) => ungroupedItem[key] === item[key])
)
const ret = [...acc, allRelatedItems]
return ret
}, [] as T[][])
}
本文标签: javascriptGroup objects by multiple properties in array then sum up their valuesStack Overflow
版权声明:本文标题:javascript - Group objects by multiple properties in array then sum up their values - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736836922a1954929.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
used
andinstances
in the sub-arrays. – Barmar Commented Oct 17, 2017 at 15:56