5

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 },
]
B-Ray
  • 473
  • 1
  • 12
  • 29
  • 1
    Your output is wrong. `aaa` has 60 and there are 2 distinct zones for `bbb`. Also, you realise you have a typo on `Selcted` – Jamiec Aug 22 '18 at 16:47

9 Answers9

2

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)
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
2

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>
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
1

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);
Jamiec
  • 133,658
  • 13
  • 134
  • 193
1

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,
       }));
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
0

Assuming your data array is named arr:

const summed = Object.values(
  arr
    .filter(x => x.Selected)
    .reduce((acc, obj) => {
      if (!acc[obj["Version Name"]) {
        acc["Version Name"] = {
          "Version Name": acc["Version Name"],
          "Zones": new Set(),
          "Value Sum": 0,
        };
      }
      acc["Version Name"]["Value Sum"] += obj.Value;
      acc["Version Name"].Zones.add(obj.Zone);
      return acc;
    })
).map(obj => {
  return {
    "Version Name": obj["Version Name"],
    "Zone Count": obj.Zones.size,
    "Value Sum": obj["Value Sum"],
  };
});

First we filter out the unSelected and get a distinct array of objects by version name by hashing on the version name, aggregating the value, using a Set to track distinct zones, then use Object.values to turn the hash back into an array. Then we map over it to get the size of each Set for the zone count.

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
0

You can use Array.reduce() and Set() for this to create a map and then Object.values() on the map will give you the desired result. try the following:

let arr = [ { Selected: false, VersionName: "aaa", Zone: "11111", Value: 5 }, { Selected: false, VersionName: "aaa", Zone: "11111", Value: 10 }, { Selected: true, VersionName: "aaa", Zone: "11111", Value: 15 }, { Selected: true, VersionName: "aaa", Zone: "11111", Value: 20 }, { Selected: true, VersionName: "aaa", Zone: "22222", Value: 25 }, { Selected: true, VersionName: "bbb", Zone: "22222", Value: 30 }, { Selected: true, VersionName: "bbb", Zone: "22222", Value: 35 }, { Selected: true, VersionName: "bbb", Zone: "2222", Value: 40 } ];
let set = new Set();

let result = Object.values(arr.reduce((a, curr)=>{
  if(curr.Selected){
    a[curr.VersionName] = a[curr.VersionName] || {versionName : curr.VersionName, ZoneCount : 0, ValueSum : 0} ;
      if(!set.has(curr.Zone+"_"+curr.VersionName)){
        a[curr.VersionName].ZoneCount += 1;
        set.add(curr.Zone+"_"+curr.VersionName);
      }
     a[curr.VersionName].ValueSum += curr.Value;
   }
  return a;
},{}));

console.log(result);
amrender singh
  • 7,949
  • 3
  • 22
  • 28
0

I think the simplest solution is to use reduce along with filter

const selectedObjects = objects.filter((i) => i.Selected === true);

const results = selectedObjects.reduce((items, obj) => {
    let item = items.find((i) => i.VersionName === obj["Version Name"]);
    if (!item) {
        const ZoneCount = selectedObjects
            .filter((i) => i["Version Name"] === obj["Version Name"])
            .reduce((zones, obj) => {
                if (zones.indexOf(obj.Zone) === -1) {
                    zones.push(obj.Zone);
                }
                return zones;
            }, []).length;

        item = {
            ValueSum: 0,
            VersionName: obj["Version Name"],
            ZoneCount
        };

        items.push(item);
    }

    item.ValueSum += obj.Value;

    return items;
}, []);

console.log(results);
Shant Marouti
  • 1,295
  • 7
  • 11
0

I feel you can do this with a single reduce, with 2 hash (since you want it in vanilla JS) and traverse the entire array only once. It seems that you want to do a filtering at the beginning, however you can do a sequence of operations like

filter(...).map(...).reduce().continue().blabla(...) (with pure js or any library like lodash, underscore... etc)

Now,

Either separate out the logic and perform cleaner operations which is easy to understand and maintain (eg: the one is there in Nina Scholz's answer)

Or, Traverse it only once with reduce (or simple for loop) and perform all the necessary operations and try avoiding series of traversal, instead use further memory to cache it like hashing (or whichever suits).

Here is a Example I have created of that

let input = [{ 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 }],
    vn = "Version Name", vs = "Value Sum", zc = "Zone Count", //Target Keys
    {res} = input.reduce(({res={}, hash={}}, e) => {
 if(e.Selected) {
  let vrsn = e[vn],
  vrsnGrp = (hash[vrsn] = hash[vrsn] || {});
  res[vrsn] = (res[vrsn] || {[vn]: vrsn, [vs]:0, [zc]: 0});
  res[vrsn][vs] += e.Value;
  res[vrsn][zc] += !vrsnGrp[e.Zone];
  vrsnGrp[e.Zone] = true;
 }
 return {res, hash};
}, {});

console.log(Object.values(res));
.as-console-wrapper { max-height: 100% !important; top: 0; }

It will perform all the necessary operations (filtering, grouping, ...) but traverse the input array only once, it will just use one extra hash (as the res is also a hash here).

Koushik Chatterjee
  • 4,106
  • 3
  • 18
  • 32
0
let campos = ["classificacao", "count_tables"];
let conteudo = mysql_information_schema[cliente][projeto][dbName].conteudo;
let count = {};
conteudo.forEach( (tabela)=> { //agrupa dentro de count
     count[tabela.classificacao] = count[tabela.classificacao] > 0 ? count[tabela.classificacao] + 1 : 1 ;
});
//converte o objeto para o padrão do array object da view
conteudo =  Object.keys(count).map( (key)=>{return {classificacao: key,  count_tables: count[key]} } );