Group objects by multiple properties in array then sum up their values
Asked Answered
H

18

59

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 ?

Hagerty answered 17/10, 2017 at 15:51 Comment(3)
You can use the solution in the other question, then at the end go through the array and sum used and instances in the sub-arrays.Turino
You could iterate through the array and use a string “shape|color” as the properties of an object.Keary
Excelent working perfetcly..... Actually I find many ways finally got the solution from your code...... Thanks a lot....Kesley
J
99

Use 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);
Jolie answered 17/10, 2017 at 15:56 Comment(4)
Why are you using a Map in the below example? What benefits does this offer compared to using a normal {} as the base reduce value? And returning Object.values(theObject) in the end. Thanks.Burglarize
If I remember correctly this answer was created when Object.values() wasn't fully supported (or I was not aware of that support). In addition using a Map has an advantage over an object - a Map would always preserve the order of entry. If a key is an integer (not the case here), a Map would preserve the entry order, while an object would arrange the keys in an ascending order.Jolie
In addition a Map will support every type of key, while an object would support only string keys (non strings are casted to strings).Jolie
A TypeScript type-safe alternative similar to this answer is discussed hereSpline
S
13

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];
          });
Stepsister answered 3/1, 2019 at 15:5 Comment(3)
This worked brilliantly. I had to group by 4 different properties and after just a few modifications to get it up to ES6 standard it worked perfectly!Renvoi
@AlfaBravo could you share the ES6 version as a separate answerLanny
Careful using JSON.stringify, the order is not guaranteed, and you might get surprises.Thacker
P
6

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)
Palter answered 17/10, 2017 at 15:57 Comment(0)
W
6

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);
Windhoek answered 20/8, 2020 at 23:17 Comment(1)
Code works fine as expected and its dynamic too. If my any value in array is NAN(to be used in sumKeys), how to handle in code?Keturahkeung
B
5

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; }
Breadbasket answered 17/10, 2017 at 15:59 Comment(0)
R
5

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];
    });
}
Renvoi answered 28/8, 2019 at 8:51 Comment(0)
T
4

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

The  result from console

Terce answered 17/9, 2020 at 14:17 Comment(0)
W
3

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];
});
Wilen answered 11/12, 2019 at 12:29 Comment(0)
S
1

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

Shuman answered 17/10, 2017 at 16:39 Comment(0)
C
1
/**
  * 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);
Cosmetic answered 10/1, 2019 at 14:26 Comment(1)
It would be nice to give some explanation why this is the solution, instead of just posting some codePlight
C
1

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
Citizenry answered 20/9, 2022 at 13:49 Comment(0)
K
0
    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....
Kesley answered 27/6, 2019 at 10:22 Comment(0)
K
0

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)
Kendalkendall answered 24/3, 2021 at 13:32 Comment(0)
T
0

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>
Thyrsus answered 10/9, 2022 at 7:19 Comment(0)
B
0

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']));
Blasien answered 22/10, 2022 at 21:5 Comment(0)
A
0
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'));
Abana answered 15/11, 2022 at 2:18 Comment(0)
P
0

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);
Plumbo answered 22/8, 2023 at 7:55 Comment(0)
T
0

Typescript version of previous answer by stretch0: https://mcmap.net/q/327444/-group-objects-by-multiple-properties-in-array-then-sum-up-their-values

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[][])
}
Tidings answered 6/2, 2024 at 17:19 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.