Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iterating over an array of objects, summing values with the same index, and returning a new array of objects

I have an array of objects, something like this:

const data = [                 // array1
  [{x: 1}, {y:2}, {z:3}], 
  [{x: 1}, {y:2}, {z:3}],
  [{x: 1}, {y:2}, {z:3}]
],[                            // array2
  [{x: 1}, {y:2}, {z:3}], 
  [{x: 1}, {y:2}, {z:3}],
  [{x: 1}, {y:2}, {z:3}]
]

What needs to be accomplished is summing x from the array1 with x from the array2 that have the same index. Same goes for y and z. The final result should be a new array of objects containing the summed values.

Something like this:

[
  [{totalXOne: 2}, {totalYOne: 4}, {totalZOne: 6}],
  [{totalXTwo: 2}, {totalYTwo: 4}, {totalZTwo: 6}],
  [{totalXThree: 2}, {totalYthree: 4}, {totalZThree: 6}],
]

Note: All arrays are the same length, and if a value is missing it will be replaced with 0)

I found something nice on MDN, but it's summing all x, y, z values, and it's returning single summed values, like this:

let initialValue = 0;
let sum = [{x: 1}, {x:2}, {x:3}].reduce(function(accumulator,currentValue) {
    return accumulator + currentValue.x;
}, initialValue)

Output:

[
  [{totalX: 3}, {totalY: 6}, {totalZ: 9}],  // this is not what I need
]

Is there any way I can achieve this?

UPDATE

I'm receiving JSON from another source. It contains a property called allEmpsData mapping over it I get the necessary salaryDataand mapping over it I'm getting the NET|GROSS|TAX data.

let allReports = [];

    setTimeout(() => {

        allEmpsData.map(x => {
            let reports = {};

            let years = [];
            let months = [];

            let netArr = [];
            let grossArr = [];
            let mealArr = [];
            let taxArr = [];
            let handSalaryArr = [];

            x.salaryData.map(y => {
                years.push(y.year);
                months.push(y.month);
                    netArr.push(y.totalNetSalary);
                    grossArr.push(y.bankGrossSalary);
                    mealArr.push(y.bankHotMeal);
                    taxArr.push(y.bankContributes);
                    handSalaryArr.push(y.handSalary);
                })
                reports.year = years;
                reports.month = months;
                reports.net = netArr;
                reports.gross = grossArr;        
                reports.meal = mealArr;        
                reports.taxesData = taxArr;        
                reports.handSalaryData = handSalaryArr;
                allReports.push(Object.assign([], reports));
        });
    }, 1000);

As I can tell, everything is working as it should, but the truth is,. I don't know any better. Then here goes the magic:

setTimeout(() => {
    result = allReports.reduce((r, a) =>
         a.map((b, i) =>
           b.map((o, j) =>
             Object.assign(...Object
              .entries(o)
               .map(([k, v]) => ({ [k]: v + (getV(r, [i, j, k]) || 0) }))
                    )
                )
            ),
            undefined
        );
            console.log(result);
        }, 1500);

... and it returns an empty array in the node console, but if I console.log any other property from the updated code above, it's there. Any suggestions?

like image 554
Dzenis H. Avatar asked Jun 10 '18 14:06

Dzenis H.


2 Answers

Here is a functional programming way to do it, using an intermediate ES6 Map:

const data = [[[{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}]], [[{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}],[{x: 1}, {y:2}, {z:3}]]];

const result = data[0].map( (arr, i) => Array.from(data.reduce( (acc, grp) => (
    grp[i].forEach( o =>
        Object.entries(o).forEach( ([k, v]) => acc.set(k, (acc.get(k) || 0) + v)) 
    ), acc
), new Map), ([k, v]) => ({ [k]: v })) ); 

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

Explanation

To facilitate the explanation let's agree on some terms:

We have the input (an array), consisting of groups. Each group is an array consisting of rows. Each row consists of objects, each having one property/value pair.

The output does not have the group level, but it has the rows, again consisting of objects, each having one property/value pair.

So using these terms let's go through the code:

As the number of rows in the output array is equal to the number of rows in any of the groups, it seems a good start to map the rows of the first group, i.e. like data[0].map.

For each row in the output, we need to make sums, and reduce is a good candidate function for that job, so we call data.reduce. For the initial value of that reduce call I have passed an empty Map. The purpose is to fill that Map with key-sum pairs. Later we can then decompose that Map into separate objects, each having one of those key/sum pairs only (but that is for later).

So the reduce starts with a Map and iterates over the groups. We need to take the ith row from each group to find the objects that must be "added". So we take the row grp[i].

Of each object in that row we get both the property name and value with Object.entries(o). In fact that function returns an array, so we iterate over it with forEach knowing that we will actually only iterate once, as there is only one property there in practice. Now we have the key (k) and value v. We're at the deepest level in the input structure. Here we adjust the accumulator.

With acc.get(k) we can know what we already accumulated for a particular key (e.g. for "x"). If we had nothing there yet, it gets initialised with 0 by doing || 0. Then we add the current value v to it and store that sum back into the Map with acc.set(k, ....). Using the comma operator we return that acc back to the reduce implementation (we could have used return here, but comma operator is more concise).

And so the Map gets all the sums per key. With Array.from we can iterate each of those key/sum pairs and, using the callback argument, turn that pair into a proper little object (with { [k]: v }). The [k] notation is also a novelty in ES6 -- it allows for dynamic key names in object literals.

So... Array.from returns an array of little objects, each having a sum. That array represents one row to be output. The map method creates all of the rows needed in the output.

like image 191
trincot Avatar answered Sep 21 '22 02:09

trincot


You could use a helper function for getting a value of a nested object and map the values at the same index.

const getV = (o, p) => p.reduce((t, k) => (t || {})[k], o);

var data = [[[{ x: 1 }, { y: 2 }, { z: 3 }], [{ x: 1 }, { y: 2 }, { z: 3 }], [{ x: 1 }, { y: 2 }, { z: 3 }]], [[{ x: 1 }, { y: 2 }, { z: 3 }], [{ x: 1 }, { y: 2 }, { z: 3 }], [{ x: 1 }, { y: 2 }, { z: 3 }]]],
    result = data.reduce((r, a) =>
        a.map((b, i) =>
            b.map((o, j) =>
                Object.assign(...Object
                    .entries(o)
                    .map(([k, v]) => ({ [k]: v + (getV(r, [i, j, k]) || 0) }))
                )
            )
        ),
        undefined
    );

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
like image 28
Nina Scholz Avatar answered Sep 18 '22 02:09

Nina Scholz