Dženis H.
Dženis H.

Reputation: 7822

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?

Upvotes: 6

Views: 384

Answers (8)

Flavio
Flavio

Reputation: 546

This solution returns a single object with each key's value being added up.

const arr1 = [
  [{x: 1}, {y: 2}, {z: 3}],
  [{x: 4}, {y: 6}, {z: null}],
  [{x: 5}, {y: 7}, {z: 9}]
]

const arr2 = [
  [{x: 12}, {y: 20}, {z: 4}],
  [{x: 13}, {y: 21}, {z: 3}],
  [{x: 14}, {y: 22}, {z: 5}]
]

const arr3 = [
  [{x: 2},    {y: 10}, {z: 67}],
  [{x: 3},    {y: 31}, {z: 23}],
  [{x: null}, {y: 25}, {z: null}]
]

function get_keys (arr) {
  let keys = []
  for (let i = 0; i < arr[0].length; i++) {
    let key = Object.keys(arr[0][i])[0]
    keys.push(key)
  }
  return keys
}

function sum_by_key (arrays) {

  let res = {}

  let keys = get_keys(arrays)
  let all_obj = []

  for (let i = 0; i < arrays.length; i++) {
    for (let d = 0; d < arrays[i].length; d++) {
      all_obj.push(arrays[i][d])
    }
  }

  for (let i = 0; i < keys.length; i++) {
    let k = keys[i]
    res[k] = 0
    for (let d = 0; d < all_obj.length; d++) {
      let __k = Object.keys(all_obj[d])[0]
      if (k === __k) {
        res[k] += all_obj[d][__k]
      }
    }
  }

  return res

}

let arrays = [...arr1, ...arr2, ...arr3]
console.log(sum_by_key(arrays)) //=> { x: 54, y: 144, z: 114 }

Upvotes: 0

Slai
Slai

Reputation: 22876

A bit shorter alternative:

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}] ] ];

var result = data.reduce( (a, b) => a.map((_, i) => 
  Array.from('xyz', (k, j) => [ { [k]: a[i][j][k] + b[i][j][k] } ] ) ) );

console.log( JSON.stringify( result ).replace(/]],/g, ']],\n ') );

Upvotes: 1

Redu
Redu

Reputation: 26161

What you ask is basically known as zipWith function. So a generic solution could be laid as;

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}]]],
    zipWith = (a,b,f) => a.map((e,i) => f(e,b[i])),
    zipper  = (sa,sb) => sa.map((o,i) => Object.keys(o)
                                               .reduce((r,k) => (r[k] = o[k] + sb[i][k], r), {})),
    result  = data.reduce((p,c) => zipWith(p,c,zipper));
console.log(result);

Upvotes: 1

Nina Scholz
Nina Scholz

Reputation: 386560

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; }

Upvotes: 3

trincot
trincot

Reputation: 350147

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.

Upvotes: 4

Numan Ijaz
Numan Ijaz

Reputation: 919

Try this simple and small code snipet:

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}]
]


var array1 = data[0];
var array2 = data[1];
var returnArray = [];

array1.forEach(function (subArray1, index){
var subArray2 = array2[index];
var subReturn = [];
subArray1.forEach(function (obj, i) {
    var variableVal;
    if (i == 0){variableVal = "x";} else if (i == 1) {variableVal = "y";}
    else if (i == 2) {variableVal = "z"}
    var newObj = {};
    newObj[variableVal] = obj[variableVal] + subArray2[i][variableVal];
    subReturn[i] = newObj;
    });
returnArray[index] = subReturn;
});

console.log(returnArray);

Upvotes: 1

amrender singh
amrender singh

Reputation: 8239

Try the following:

var arr1 = [[{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}]];

var arr2 = [[{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}], [{x: 1}, {y:2}, {z:3}]];

var map = {
  0 : 'x',
  1 : 'y',
  2 : 'z'
};
var map2 = {
 0 :"One",
 1 :"Two",
 2 : "Three"
};
var result = [];
var obj= {};

for(var i = 0; i < arr1.length; i++){
  total = 0;
  var arr =[];
  for(var j =0; j < arr1[i].length; j++){
    obj["total"+ map[j] + map2[i]] = arr1[i][j][map[j]] + arr2[i][j][map[j]];
     arr.push(obj);
     obj = {};
  }
  result.push(arr);
}
console.log(result);

Upvotes: 2

Duncan Thacker
Duncan Thacker

Reputation: 5188

It's a good idea to try and break this sort of problems down into smaller problems, and build up gradually. This means we don't have to look at the whole thing in one go.

Let's write a function that adds together individual elements from an array:

function addElements(element1, element2, key, rowIndex) {
   //for now we keep the keys the same, otherwise multiple additions
   //won't work   
   return {
       [key]: element1[key] + element2[key]
   };
}

Now let's add two rows together, using our addElements():

function addRows(row1, row2, rowIndex) {
    return ['x', 'y', 'z'].map((key, index) => {
        // "key" will go through "x", "y", and "z" as
        // "index" goes 0, 1, 2
        const element1 = row1[index];
        const element2 = row2[index];
        return addElements(element1, element2, key, rowIndex);
    });
}

Now we can iterate through all the rows in our first matrix, and add the equivalent from the second matrix using addRows():

function addMatrices(matrix1, matrix2) {
     return matrix1.map((row1, index) => {
         const row2 = matrix2[index];
         return addRows(row1, row2, index);
     });
}

Now we can turn this into a reducer:

const EMPTY_MATRIX = { ... }; //define a matrix of all zeroes here
matrices.reduce(addMatrices, EMPTY_MATRIX);

Hope this helps!

Upvotes: 1

Related Questions