1

I have an array of objects coming from a database that is stored in an angular scope variable. The array format :

$scope.Divisions = [
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 2" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 3" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN3", "KPI": "Div1 KPI 5" },

    { "Division": "Div2", "Data Name": "DN3", "KPI": "Div2 KPI 6" },
    { "Division": "Div2", "Data Name": "DN4", "KPI": "Div2 KPI 6" },

    { "Division": "Div3", "Data Name": "DN3", "KPI": "Div3 KPI 7" },

    { "Division": "Div4", "Data Name": "DN3", "KPI": "Div4 KPI 7" },
]

The transformed array I need is to get distinct Data Name and distinct KPI count for each Division in the below format:

[

    ['Div1', 3, 5],
    ['Div2', 2, 1],
    ['Div3', 1, 1],
    ['Div4', 1, 1]
  ]
6
  • this seems like a loop to me -- did you try that? Did you have an issue with it? Commented Jun 3, 2020 at 19:23
  • developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/… Commented Jun 3, 2020 at 19:28
  • You probably want for..in, but any loop could be used. Commented Jun 3, 2020 at 19:29
  • How the 2nd entry is calculated? ['Div1', 3, 5] Commented Jun 3, 2020 at 19:33
  • 1
    Don't mind, but this is pure logic building, you should do it yourself. 🙂 If this is done by something else, then it is not helping, it is like doing your job. 😇 Commented Jun 3, 2020 at 19:39

2 Answers 2

1

Example of one way to do it, using reduce and map (slower than loops due to function calls but very expressive & functional style and for small arrays, you will never notice the performance difference):

In essence:

  • create constants for the keys/properties to count const a = "Data Name"; const b = "KPI";
  • generate an empty object and populate it by looping over the items in the input array (using reduce here or for loop below)
  • each time check if the property of the Division is already present in the object (e.g. check if the object has the prop/key "Div1" first etc.)
  • if it is not present, add this item to the object with that property and convert the item[a] and item[b] to single item arrays. (where a and b are defined above)
  • if it is already present, then push on to item[a] and item[b] (only if the value is not in the array by checking that .indexOf() == -1 ).
  • convert this Object to an array of values
  • map over the array (with map here or for loop below) and convert to [item.Division, item[a].length, item[b].length]

const Divisions = [
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 2" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 3" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN3", "KPI": "Div1 KPI 5" },
    { "Division": "Div2", "Data Name": "DN3", "KPI": "Div2 KPI 6" },
    { "Division": "Div2", "Data Name": "DN4", "KPI": "Div2 KPI 6" },
    { "Division": "Div3", "Data Name": "DN3", "KPI": "Div3 KPI 7" },
    { "Division": "Div4", "Data Name": "DN3", "KPI": "Div4 KPI 7" },
];

const a = "Data Name";
const b = "KPI";
const tempOutput = Divisions.reduce((aggObj, item) => {
  
  if (aggObj[item.Division]){
    if (aggObj[item.Division][a].indexOf(item[a]) == -1 ){
      aggObj[item.Division][a].push(item[a]);
    }
    if (aggObj[item.Division][b].indexOf(item[b]) == -1 ){
      aggObj[item.Division][b].push(item[b]);
    }
  }
  else {
    aggObj[item.Division] = item;
    aggObj[item.Division][a] = [item[a]];
    aggObj[item.Division][b] = [item[b]];
  }
  
  return aggObj;
}, {});

const output = Object.values(tempOutput).map(item => {  
  return [item.Division, item[a].length, item[b].length];
});

//console.log(tempOutput)
console.log(output)

Output:

[
  ["Div1", 3, 5],
  ["Div2", 2, 1],
  ["Div3", 1, 1],
  ["Div4", 1, 1]
]

Example of another way to do it, using for loop (faster, usually easier if coming from another language etc.):

const Divisions = [
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 2" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 3" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN3", "KPI": "Div1 KPI 5" },
    { "Division": "Div2", "Data Name": "DN3", "KPI": "Div2 KPI 6" },
    { "Division": "Div2", "Data Name": "DN4", "KPI": "Div2 KPI 6" },
    { "Division": "Div3", "Data Name": "DN3", "KPI": "Div3 KPI 7" },
    { "Division": "Div4", "Data Name": "DN3", "KPI": "Div4 KPI 7" },
];

const a = "Data Name";
const b = "KPI";
const aggObj = {};

for (let item of Divisions){
  
  if (aggObj[item.Division]){
    if (aggObj[item.Division][a].indexOf(item[a]) == -1 ){
      aggObj[item.Division][a].push(item[a]);
    }
    if (aggObj[item.Division][b].indexOf(item[b]) == -1 ){
      aggObj[item.Division][b].push(item[b]);
    }
  }
  else {
    aggObj[item.Division] = item;
    aggObj[item.Division][a] = [item[a]];
    aggObj[item.Division][b] = [item[b]];
  }

}

const tempOutput = Object.values(aggObj);
const output = [];
for (let item of tempOutput){  
  output.push([item.Division, item[a].length, item[b].length]);
}

//console.log(tempOutput)
console.log(output)

Output:

[
  ["Div1", 3, 5],
  ["Div2", 2, 1],
  ["Div3", 1, 1],
  ["Div4", 1, 1]
]
Sign up to request clarification or add additional context in comments.

11 Comments

excellent updates alex! -- just a note about performance. map/reduce was designed to be performant when you had clusters of machines solving big problems. That is why it does not work well on a problem like this -- cf en.wikipedia.org/wiki/MapReduce
@AlexL what he said has truth to it. The concept was popularized by big data, esp. when Google published their data processing optimization by use of Map Reduce to enable moving large amounts of data locally to be transformed before being transferred, where network transfers and I/O were the main bottleneck. It also simplified the way data transformations were represented and done, and so it was a highly productive pattern. He comes from a db admin background, so that's probably where he's coming from.
Ok, I am learning a lot today :) - So map, reduce etc. (higher order array methods) are slower than for loops because of function calls and object initialization. Also map & reduce were developed with functional style and parallel computing in mind (I assume because we want pure functions, no side effects etc.)? - I don't fully understand how reduce can be parallelized when the aggregator is an object and we check if the property is already present etc.? (But perhaps that's an example of what you don't do as it would not be pure?) Map is easy to think about being parallelized.
There's also some things like native array optimizations, where they can be an order of magnitude more performant. Depends on the JS engine.
Gotchya, so V8 might have optimized .map() to be faster than for loops as long as it is an array of all small integers etc.? v8.dev/blog/elements-kinds (But SpiderMonkey might not have optimized it and so it might be slower than for loops there etc.?)
|
1

Use an object map with key: [Division] to track Divisions. Use Set()s to track duplicates. Map to .size() to get counts.
Detailed commented code at bottom.

Divisions = [
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 2" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 3" },
    { "Division": "Div1", "Data Name": "DN1", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 1" },
    { "Division": "Div1", "Data Name": "DN2", "KPI": "Div1 KPI 4" },
    { "Division": "Div1", "Data Name": "DN3", "KPI": "Div1 KPI 5" },

    { "Division": "Div2", "Data Name": "DN3", "KPI": "Div2 KPI 6" },
    { "Division": "Div2", "Data Name": "DN4", "KPI": "Div2 KPI 6" },

    { "Division": "Div3", "Data Name": "DN3", "KPI": "Div3 KPI 7" },

    { "Division": "Div4", "Data Name": "DN3", "KPI": "Div4 KPI 7" },
]

console.log(
 Object.values(
  Divisions.reduce((acc,{Division,['Data Name']:Name,KPI}) => {
    const [,Names,KPIs] = 
      acc[Division] = acc[Division] || [Division,new Set(),new Set()]
    Names.add(Name)
    KPIs.add(KPI)
    return acc
  },{})
 ).map(([div,names,kpis])=>([div,names.size,kpis.size]))
)

   // unwrap object map to values
   Object.values(
    // iterate over Divisions objects and create object map key: [Division] to track Divisions
    Divisions.reduce((acc,{Division,['Data Name']:Name,KPI}) => {
      // destructure Division 'Data Name' and KPI properties

      // destructure Names and KPIs Set()s
      const [,Names,KPIs] =
        acc[Division] = acc[Division] || [Division,new Set(),new Set()]
        // add to object map with key: [Division] and Set()s for Names and KPIs
      Names.add(Name)
      KPIs.add(KPI)
      return acc
    },{})
   // map Names Set() and KPIs Set() to .size count (number of occurrences)
   ).map(([div,names,kpis])=>([div,names.size,kpis.size]))

1 Comment

Using a Set is much cleaner than checking if the .indexOf() == -1 👌 up-voted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.