-1

I am trying to group data by multiple properties and sum their values.

Here is what I tried as per this question

I had a follow up to this question:

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);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

I wanted to make this more reusable with the numerical values. In this for example, I want the

const item = r.get(key) || Object.assign({}, o, {
    used: 0,
    instances: 0
  });
  
  item.used += o.used;
  item.instances += o.instances;

part especially to be reusable.

I got the numerical value keys in an array: let gee = ['used', 'instances'];

I am not sure how to use it with Object.assign. I tried to do this:

const result = [...arr.reduce((r, o) => {
        const key = o.shape + '-' + o.color;
        // console.log(o);
        const item = gee.forEach(v => o[v] += o[v]);
        // 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()];

But this is not working. How can I use an array for this bit of code:

const item = r.get(key) || Object.assign({}, o, {
    used: 0,
    instances: 0
  });
  
  item.used += o.used;
  item.instances += o.instances;
nb_nb_nb
  • 1,243
  • 11
  • 36

3 Answers3

2

If the Map object has the key, loop through the totalKeys and increment the object in the accumulator with current object's data. If it is new key, add a copy of the object to the Map

if (r.has(key)) {
  const item = r.get(key)
  totalKeys.forEach(k => item[k] += o[k])
} else {
  r.set(key, { ...o })
}

Here's a snippet:

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 groupSum(array, totalKeys) {
  const group = arr.reduce((r, o) => {
    const key = o.shape + '-' + o.color;

    if (r.has(key)) {
      const item = r.get(key)
      totalKeys.forEach(k => item[k] += o[k])
    } else {
      r.set(key, { ...o })
    }
    
    return r;
  }, new Map);

  return Array.from(group.values())
}


console.log(
  groupSum(arr, ['used', 'instances'])
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

You can make it even more dynamic by providing an array of keys to group by. Create the key using the values of the object separated by a |

const key = groupKeys.map(k => o[k]).join("|");

if (r.has(key)) {
  const item = r.get(key)
  totalKeys.forEach(k => item[k] += o[k])
} else {
  r.set(key, { ...o })
}

Here's a snippet:

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 groupSum(array, groupKeys, totalKeys) {
  const group = arr.reduce((r, o) => {
    const key = groupKeys.map(k => o[k]).join("|");

    if (r.has(key)) {
      const item = r.get(key)
      totalKeys.forEach(k => item[k] += o[k])
    } else {
      r.set(key, { ...o })
    }
    
    return r;
  }, new Map);

  return Array.from(group.values())
}


console.log(
  groupSum(arr, ['shape', 'color'], ['used', 'instances'])
)
adiga
  • 34,372
  • 9
  • 61
  • 83
1

Maybe this would be a way to go:

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}],
nums=["used","instances"]


function summationOn(ar,cnts){ // cnts: add up counts on these properties
 const grp=Object.keys(ar[0]).filter(k=>cnts.indexOf(k)<0) // grp: group over these
 return Object.values(ar.reduce((a,c,t)=>{
  const k=grp.map(g=>c[g]).join("|");
  if (a[k]) cnts.forEach(p=>a[k][p]+=c[p])
  else a[k]={...c};
  return a
 },{}))
}

const res=summationOn(arr,nums);
console.log(res);

re-write
Similar to @adiga I now expect the "countable" properties to be given in the array cnts. With this array I collect all other properties of the first object of input array ar into array grp. These are the properties I will group over.

Carsten Massmann
  • 26,510
  • 2
  • 22
  • 43
  • They have an array of properties they want to sum. `let gee = ['used', 'instances']`. They don't want to hard code like `t.used+=c.used` – adiga May 07 '21 at 17:36
  • Ah - thanks for letting me know. So, how do I identify the properties for summation?!? – Carsten Massmann May 07 '21 at 17:37
  • 1
    You can send an array of properties to sum. Loop through the array and get the properties dynamically. You can even use an array of properties to group by `["shape", "color"]`. Check the snippets in my answer – adiga May 07 '21 at 17:38
1

You could vastly simplify the dataset too by not using the combination of array.reduce() with a map()... and instead just build your new array by looping through all elements of the original array with array.forEach().

I added your use of the gee array as being a list of numeric fields you want to have added... to include making sure they exist on every object of the result array...whether or not they existed on each of the previous objects in arr.

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,
  "testProp": 1
}, {
  "shape": "square",
  "color": "blue",
  "used": 4,
  "instances": 5
}, {
  "shape": "square",
  "color": "red",
  "used": 2,
  "instances": 1
}];

let gee = ['used', 'instances', 'testProp'];
let result = [];

arr.forEach((o) => {
  // Setup TempSource since not all o may have all elements in gee
  let tempSource = {};
  gee.forEach((key) => {
    if (o.hasOwnProperty(key)) {
      tempSource[key] = o[key];
    } else {
      tempSource[key] = 0;
    }
  });

  // Look to see if the result array already has an object with same shape/color
  const matchingObject = result.find(element => {
    let returnValue = true;
    returnValue &= (element.shape == o.shape);
    returnValue &= (element.color == o.color);
    return returnValue;
  });

  if (matchingObject) {
    // Matching Object already exists... so increment values
    gee.forEach((key) => {
      matchingObject[key] += tempSource[key];
    });
  } else {
    // Matching Object missing, so merge newObject and insert
    let newObj = {};
    Object.assign(newObj, o, tempSource);
    result.push(newObj);
  }
});

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
DynasticSponge
  • 1,416
  • 2
  • 9
  • 13