32

I have an array of objects that looks like this:

var data = [
    { costOfAirtickets: 2500, costOfHotel: 1200 },
    { costOfAirtickets: 1500, costOfHotel: 1000 },
]

and I want to sum each element in the array to produce an array like this:

var result = [ { costOfAirtickets: 4000, costOfHotel: 2200 } ]

I have used a map and reduce function but I was able to only sum an individual element like so:

data.map(item => item.costOfAirtickets).reduce((prev, next) => prev + next); // 22

At the moment this produces a single value which is not what I want as per initial explanation.

Is there a way to do this in Javascript or probably with lodash.

Matt
  • 8,758
  • 4
  • 35
  • 64
Dave Kalu
  • 1,520
  • 3
  • 19
  • 38

14 Answers14

21

Using for..in to iterate object and reduce to iterate array

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];

var result = [data.reduce((acc, n) => {
  for (var prop in n) {
    if (acc.hasOwnProperty(prop)) acc[prop] += n[prop];
    else acc[prop] = n[prop];
  }
  return acc;
}, {})]
console.log(result)
Chris Li
  • 2,628
  • 1
  • 8
  • 23
  • 2
    This solution is nice, because it allows one to automatically include different properties per trip (`{..., costOfRentalCar: 300}`) . But it only works if the *only* properties in a trip are costs. – Scott Sauyet Sep 24 '18 at 12:29
  • 2
    Instead of `acc[prop]`, use [`acc.hasOwnProperty(prop)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty). This prevents that, in some situations, you are checking if there's a `__toString` or something. Also, use `acc[prop] += +n[prop] || 0;` for summing. If I pass a string to it, it will just concatenate. The `+n[prop]` forces it to be converted to integer. The `|| 0` removes other falsy values like NaN. Same thing for `acc[prop] = n[prop]` to be `acc[prop] = +n[prop] || 0`. – Ismael Miguel Sep 24 '18 at 15:01
  • 2
    This answer is several years out of date. People don't use `for...in` + `hasOwnProperty` anymore, use `for(const key of Object.keys(...))` instead. Also this whole thing can be done as a simple one-liner using lodash. – BlueRaja - Danny Pflughoeft Sep 24 '18 at 19:06
  • 1
    @IsmaelMiguel Even simpler, `if (prop in acc) …` – Bergi Sep 24 '18 at 19:41
  • @Bergi That's not *better*. That's *exactly* what you *shouldn't* do. Try `'toString' in {}`. If it returns `true`, then your code probably can break. `if(proc in acc)` checks if a property exists in the object ***and*** prototype(s). Something you *don't* want here. – Ismael Miguel Sep 26 '18 at 00:17
  • @IsmaelMiguel I said "simpler", not "better". Apparently you already know all the property names beforehand, so you can rule out `toString` or `hasOwnProperty` from appearing. – Bergi Sep 26 '18 at 10:07
  • But what if I do `{'toString': 1}`? Or `{'hasOwnProperty': 1}`? Using `({'toString': 1}).hasOwnProperty('toString')` gives `true`. This means that you are intentionally skipping things. – Ismael Miguel Sep 26 '18 at 11:44
8

Use Lodash to simplify your life.

const _ = require('lodash')
let keys = ['costOfAirtickets', 'costOfHotel']; 
let results = _.zipObject(keys, keys.map(key => _.sum( _.map(data, key))))
...
{ costOfAirtickets: 4000, costOfHotel: 2200 }

Explanation:

  • _.sum(_.map(data, key)): generates sum of each array
  • _.zipObject: zips the results with the array sum
  • using keys.map() for sum of each key as _.map does not guarantee order.

Documentation:

Roopak A Nelliat
  • 2,009
  • 3
  • 19
  • 26
  • Your solution doesn't solve the problem asked by the OP which was how to sum an array of objects like such: `var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}]` – Dave Kalu Sep 24 '18 at 13:50
  • 2
    @DaveKalu: why? `result` contains exactly the asked value. Aren't you the OP? IMO this is the best answer. @Roopak: you can still simplify it a little bit: `_.sumBy(data, key)` or `_(data).sumBy(key)`. – tokland Sep 24 '18 at 19:09
  • 2
    @DaveKalu: This does solve the exact problem asked. It's also the cleanest answer on this page, and exactly what I was about to write. Note that `keys` can be defined as `const keys = Object.keys(data);` – BlueRaja - Danny Pflughoeft Sep 24 '18 at 19:37
  • This might be one of the best approach but it needs the keys to be known against which we will perform the sum. In such case (assuming) the other answers can be optimized too (since most of the answers are targeting sum of all the unique properties). Knowing the properties (as input) to perform the sum, will give the advantage that it can be started from the array of properties rather than starting from the actual array (like this solution). – Koushik Chatterjee Sep 24 '18 at 20:03
7

You don't need to map the array, you're really just reducing things inside it.

const totalCostOfAirTickets: data.reduce((prev, next) => prev + next.costOfAirTickets, 0)
const totalCostOfHotel: data.reduce((prev, next) => prev + next.costOfHotel, 0)
const totals = [{totalCostOfAirTickets, totalCostOfHotel}]

In one go, you could do something like

const totals = data.reduce((prev, next) => { 
    prev.costOfAirTickets += next.costOfAirTickets; 
    prev.costOfHotel += next.costOfHotel; 
}, {costOfAirTickets: 0, costOfHotel: 0})
Cyril Gandon
  • 16,830
  • 14
  • 78
  • 122
Simon
  • 71
  • 2
6

To create a resultant / reduced value, you should use .reduce() method instead of .map():

let data = [
  {costOfAirtickets: 2500, costOfHotel: 1200},
  {costOfAirtickets: 1500, costOfHotel: 1000}
];

let result = data.reduce(
  (a, c) => (Object.keys(c).forEach(k => (a[k] = (a[k] || 0) + c[k])), a), {}
);

console.log(result);
Mohammad Usman
  • 37,952
  • 20
  • 92
  • 95
5

Another solution would be to use Map (not Array.prototype.map) as it has several notable differences compared to objects:

var data = [{
  costOfAirtickets: 2500,
  costOfHotel: 1200
}, {
  costOfAirtickets: 1500,
  costOfHotel: 1000
}]

let sums = data.reduce((collection,rcd)=>{
  Object.entries(rcd).forEach(([key,value])=>{
      let sum = collection.get(key) || 0
      collection.set(key, sum + +value)
  })
  return collection
}, new Map())

console.log(...sums.entries())

Explanation

Outer loop

The above first iterates over your data array using the reduce method. Each object within that I'll be referring to as a record -- distinguished in the code via the variable, rcd.

Each iteration of reduce returns a value which is passed as the first argument to the next iteration of the loop. In this case, the parameter collection holds that argument, which is your set of sums.

Inner loop

Within the reduce loop, each key/value pair of the record is iterated over using forEach. To get the key/value pair the Object.entries method is used. Using array destructuring these arguments can be directly assigned to the respective variables, key and value

Retrieving/Setting values

Unlike a primitive object, Map has its own methods for getting and setting its entries using get() and set(). So first retrieve the previous sum using get(), if it's not set then default to 0, which is what the || 0 does. At that point, you can assume the previous sum is at least 0 or greater and add the current key's value onto it.

Alternatives to Map

If you find Map is a bit heavy-handed, you may also use a similar object such as Set, which has many of the same methods (except the get()), or you could also use a primitive object (i.e. {}).

Mike
  • 1,279
  • 7
  • 18
4

Here is a lodash approach

_(data).flatMap(_.entries).groupBy(0).mapValues(v=>_.sumBy(v, 1)).value()

It will sum by all the unique keys.

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];

var res = _(data).flatMap(_.entries).groupBy(0).mapValues(v=>_.sumBy(v, 0)).value();

console.log(res);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

Wrap your result to a [...] or use a .castArray() at the end before unwrapping using .value() in case you want a array as result.

Koushik Chatterjee
  • 4,106
  • 3
  • 18
  • 32
2

You could reduce the array by taking an object for summing.

var data = [{ costOfAirtickets: 2500, costOfHotel: 1200 }, { costOfAirtickets: 1500, costOfHotel: 1000 }],
    keys = ['costOfAirtickets', 'costOfHotel'],
    sum = data.reduce((r, o) => {
        keys.forEach(k => r[k] += o[k]);
        return r;
    }, Object.assign(...keys.map(k => ({ [k]: 0 }))));

console.log(sum);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Should be the best answer. Easily customizable and dynamic. – Shinjo Aug 12 '19 at 09:19
  • When using typescript to shutdown error: `Expected at least 1 arguments, but got 0 or more.ts(2557) lib.es2015.core.d.ts(313, 12): An argument for 'target' was not provided.` on `Object.assign(...keys` you can do `Object.assign({}, ...keys` – Shinjo Aug 12 '19 at 10:40
  • right, but the whole makes no sense, if you haven't some keys. – Nina Scholz Aug 12 '19 at 10:44
1

You can use a simple forEach() loop for that:

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];
var res = [];
var tempObj = {};
data.forEach(({costOfAirtickets, costOfHotel}) => {
  tempObj['costOfAirtickets'] = (tempObj['costOfAirtickets'] || 0) + costOfAirtickets;
  tempObj['costOfHotel'] = (tempObj['costOfHotel'] || 0) + costOfHotel;
 });
res.push(tempObj);
console.log(res);
Ankit Agarwal
  • 30,378
  • 5
  • 37
  • 62
1

Try using reduce only as in the below snippet. Try avoiding multiple iterations. Hope the below snippet helps!

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}]

var total = data.reduce(function (result,value,key) {
result['costOfAirtickets'] = result['costOfAirtickets']  + value['costOfAirtickets'];
result['costOfHotel'] = result['costOfHotel']  + value['costOfHotel'];

return result},{costOfAirtickets:0,costOfHotel:0});

console.log(total)
CRayen
  • 569
  • 2
  • 11
1

map the object and calculate the sum and store it in another.

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];
var result = [];
var sum = 0;
var costsum = 0;
data.map(function(item, key){
  var cost = item;
  //nsole.log(cost);
  sum = sum + cost.costOfAirtickets;
  costsum = costsum + cost.costOfHotel;

});

result = [{costOfAirtickets:sum, costOfHotel:costsum}];

console.log(result);
chintuyadavsara
  • 1,509
  • 1
  • 12
  • 23
1

This is the easiest approach I could think off

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];
var sum ={};
for(var obj in data){
  for(var ele in data[obj]){
    if(!data[obj].hasOwnProperty(ele)) continue;
      if(sum[ele] === undefined){
        sum[ele] = data[obj][ele];
      }else{
        sum[ele] = sum[ele] + data[obj][ele];
      }
  }

}
var arr = [sum];
console.log(arr);
Armando Prieto
  • 129
  • 1
  • 4
1

My simplistic answer would be like;

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},
            {costOfAirtickets: 1500, costOfHotel: 1000}],
    res  = data.reduce((p,c) => Object.entries(c).reduce((r,[k,v]) => (r[k]+=v, r), p));
console.log(res);

A note on usage of .reduce(): If the array items and the accumulated value is of same type you may consider not using an initial value as an accumulator and do your reducing with previous (p) and current (c) elements. The outer reduce in the above snippet is of this type. However the inner reduce takes an array of key (k) value (v) pair as [k,v] to return an object, hence an initial value (p) is essential.

The result is the accumulated value as an object. If you need it in an array then just put it in an array like [res].

Redu
  • 25,060
  • 6
  • 56
  • 76
1

Lodash

Using lodash reduce and _.mergeWith this is a one liner:

var data = [{costOfAirtickets: 2500, costOfHotel: 1200}, {costOfAirtickets: 1500, costOfHotel: 1000}]

var result = _.reduce(data, (r,c) => _.mergeWith(r, c, (o = 0, s) => o + s), {})

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

ES6 Only

If you do NOT want to mutate the original array you can utilize ES6 reduce, Object.entries, and forEach like this:

var data = [{costOfAirtickets: 2500, costOfHotel: 1200}, {costOfAirtickets: 1500, costOfHotel: 1000}]

// One liner
var result1 = data.reduce((r, c) =>
  !Object.entries(c).forEach(([key, value]) => r[key] = (r[key] || 0) + value) && r, {})

// More readable
var result2 = data.reduce((r, c) => {
  Object.entries(c).forEach(([key, value]) => r[key] = (r[key] || 0) + value)
  return r
}, {})

console.log(result1)
console.log(result2)

If we do not care about mutating the initial data array then we can have a one liner solution:

var data = [{costOfAirtickets: 2500, costOfHotel: 1200}, {costOfAirtickets: 1500, costOfHotel: 1000}]

data.reduce((r, c) => !Object.entries(c).forEach(([key,value]) => r[key] += value) && r)

console.log(data[0])

More readable:

var data = [{costOfAirtickets: 2500, costOfHotel: 1200}, {costOfAirtickets: 1500, costOfHotel: 1000}]

data.reduce((r, c) => {
  Object.entries(c).forEach(([key, value]) => r[key] += value)
  return r
})

console.log(data[0])

The only difference between the mutating and not mutating examples is the initial value for the reduce (and also the fact that with the mutating we use the 0 index to as an accumulator for the sums). In the mutating ones there is no initial value where in the others we start with empty object literal.

if you need the result to be an array specifically then return [data] for the mutating examples and [result] for the pure examples.

Akrion
  • 18,117
  • 1
  • 34
  • 54
0

var data = [{costOfAirtickets: 2500, costOfHotel: 1200},{costOfAirtickets: 1500, costOfHotel: 1000}];

var result = [data.reduce((acc, n) => {
  for (var prop in n) {
    if (acc[prop]) acc[prop] += n[prop];
    else acc[prop] = n[prop];
  }
  return acc;
}, {})]
console.log(result)