Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Group By, Count Distinct, Sum Totals From Objects in JavaScript Array

Tags:

javascript

This is probably remedial but I can't figure it out. I've tried using d3 and played with lodash to get an efficient solution, but didn't get anything close.

I have an array of objects in JavaScript. If the [Selected] value is true, I want to create an object grouped by [Version Name] with a count of distinct zones, and a sum of the totals per version. For instance with object...

[ 
     { Selcted: false, Version Name: "aaa", Zone: "11111", Value: 5 },
     { Selcted: false, Version Name: "aaa", Zone: "11111", Value: 10 },
     { Selcted: true, Version Name: "aaa", Zone: "11111", Value: 15 },
     { Selcted: true, Version Name: "aaa", Zone: "11111", Value: 20 },
     { Selcted: true, Version Name: "aaa", Zone: "22222", Value: 25 },
     { Selcted: true, Version Name: "bbb", Zone: "22222", Value: 30 },
     { Selcted: true, Version Name: "bbb", Zone: "22222", Value: 35 },
     { Selcted: true, Version Name: "bbb", Zone: "2222", Value: 40 }
]

Should return a result of

[ 
     { Version Name: "aaa", Zone Count: "2", Value Sum: 50 },
     { Version Name: "bbb", Zone Count: "1", Value Sum: 105 },
]
like image 655
B-Ray Avatar asked Aug 22 '18 16:08

B-Ray


4 Answers

This uses my favorite groupBy function :) Once you get the groups, you do another group to get the zone count, and a reduce to get your sum.

In a nutshell

const byName = groupBy(input.filter(it => it.Selcted), it => it['Version Name'])

const output = Object.keys(byName).map(name => {
  const byZone = groupBy(byName[name], it => it.Zone)
  const sum = byName[name].reduce((acc, it) => acc + it.Value, 0)
  return {
    'Version Name': name,
    ZoneCount: Object.keys(byZone).length,
    ValueSum: sum
  }
})

Don't forget, you need quotes around 'Version Name' to use it as a key.

Here's a working example with your dataset.

function groupBy(a, keyFunction) {
  const groups = {};
  a.forEach(function(el) {
    const key = keyFunction(el);
    if (key in groups === false) {
      groups[key] = [];
    }
    groups[key].push(el);
  });
  return groups;
}

const input = [{
    Selcted: false,
    'Version Name': "aaa",
    Zone: "11111",
    Value: 5
  },
  {
    Selcted: false,
    'Version Name': "aaa",
    Zone: "11111",
    Value: 10
  },
  {
    Selcted: true,
    'Version Name': "aaa",
    Zone: "11111",
    Value: 15
  },
  {
    Selcted: true,
    'Version Name': "aaa",
    Zone: "11111",
    Value: 20
  },
  {
    Selcted: true,
    'Version Name': "aaa",
    Zone: "22222",
    Value: 25
  },
  {
    Selcted: true,
    'Version Name': "bbb",
    Zone: "22222",
    Value: 30
  },
  {
    Selcted: true,
    'Version Name': "bbb",
    Zone: "22222",
    Value: 35
  },
  {
    Selcted: true,
    'Version Name': "bbb",
    Zone: "2222",
    Value: 40
  }
]

const byName = groupBy(input.filter(it => it.Selcted), it => it['Version Name'])

const output = Object.keys(byName).map(name => {
  const byZone = groupBy(byName[name], it => it.Zone)
  const sum = byName[name].reduce((acc, it) => acc + it.Value, 0)
  return {
    'Version Name': name,
    ZoneCount: Object.keys(byZone).length,
    ValueSum: sum
  }
})


console.log(output)
like image 66
Steven Spungin Avatar answered Sep 28 '22 04:09

Steven Spungin


You could use lodash and get the wanted counts after grouping by using

  • _ “Seq” Methods, for chaining lodash methods,
  • _.groupBy for grouping by "Version Name"
  • _.map for the result sets with
  • _.uniqBy for counting distinct values,
  • _.sumBy for summing Value and
  • _.value for getting an array with objects as result set.

var data = [{ Selected: false, "Version Name": "aaa", Zone: "11111", Value: 5 }, { Selected: false, "Version Name": "aaa", Zone: "11111", Value: 10 }, { Selected: true, "Version Name": "aaa", Zone: "11111", Value: 15 }, { Selected: true, "Version Name": "aaa", Zone: "11111", Value: 20 }, { Selected: true, "Version Name": "aaa", Zone: "22222", Value: 25 }, { Selected: true, "Version Name": "bbb", Zone: "22222", Value: 30 }, { Selected: true, "Version Name": "bbb", Zone: "22222", Value: 35 }, { Selected: true, "Version Name": "bbb", Zone: "22222", Value: 40 }],
    result = _(data)
        .groupBy('Version Name')
        .map((array, key) => ({
            "Version Name": key,
            "Zone Count": _.uniqBy(array, 'Zone').length,
            "Value Sum": _.sumBy(array, 'Value')
        }))
        .value();

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>
like image 36
Nina Scholz Avatar answered Sep 28 '22 04:09

Nina Scholz


Having used filter to get the items you want, you can do this in two steps, with a reduce followed by a map

let input = [ 
     { Selcted: false, "Version Name": "aaa", Zone: "11111", Value: 5 },
     { Selcted: false, "Version Name": "aaa", Zone: "11111", Value: 10 },
     { Selcted: true, "Version Name": "aaa", Zone: "11111", Value: 15 },
     { Selcted: true, "Version Name": "aaa", Zone: "11111", Value: 20 },
     { Selcted: true, "Version Name": "aaa", Zone: "22222", Value: 25 },
     { Selcted: true, "Version Name": "bbb", Zone: "22222", Value: 30 },
     { Selcted: true, "Version Name": "bbb", Zone: "22222", Value: 35 },
     { Selcted: true, "Version Name": "bbb", Zone: "2222", Value: 40 }
]

var result = input.filter(x => x.Selcted)
                  .reduce( (acc, curr) => {
                      let item = acc.find(x => x.version == curr["Version Name"]);
                      if(!item){
                          item = {version: curr["Version Name"], zones:{}}
                          acc.push(item);
                      }
                      item.zones[curr.Zone] = (item.zones[curr.Zone] || 0) + curr.Value
                      return acc;
                  },[])
                  .map(x => ({
                    "Version Name": x.version,
                    "Zone Count": Object.keys(x.zones).length,
                    "Value Sum": Object.values(x.zones).reduce( (a,b) => a+b ,0)
                  }))
                  
console.log(result);
like image 27
Jamiec Avatar answered Sep 28 '22 03:09

Jamiec


First filter out, then group, then count the zones, for that we need some heler functions on arrays:

  Array.prototype.groupBy = function groupBy(key) {
    const hash = {}, result = [];
    for(const el of this) {
       if(hash[ el[key] ]) {
         hash[ el[key] ].push(el);
       } else {
         result.push({
           key: el[key],
           values: hash[ el[key] ] = [ el ],
        });
     }
  }
  return result;
 };

 Array.prototype.key = function(key) {
   return this.map(el => el[key]);
 };

 Array.prototype.sum = function(key) {
  return this.reduce((total, el) => total + (key ? el[key] : el), 0);
 };

 Array.prototype.unique = function() {
   return [...new Set(this)];
 };

That is actually quite a lot of code, but now we can use that to build up our result:

  const result = array
       .filter(el =>  el.Selected)
       .groupBy("Version Name")
       .map(({ key, values }) => ({
         "Version Name": key,
         "Value Sum": values.sum("Sum"),
         "Zone Count":values.key("Zone").unique().length,
       }));
like image 31
Jonas Wilms Avatar answered Sep 28 '22 02:09

Jonas Wilms