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
  • 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. – Barmar Commented Oct 17, 2017 at 15:56
  • You could iterate through the array and use a string “shape|color” as the properties of an object. – Eric Zhang Commented Oct 17, 2017 at 15:57
  • Excelent working perfetcly..... Actually I find many ways finally got the solution from your code...... Thanks a lot.... – Mahendren Mahisha Commented Jun 27, 2019 at 10:14
Add a comment  | 

18 Answers 18

Reset to default 100

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);

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