Silviu Burcea
Silviu Burcea

Reputation: 5348

Sum array objects by multiple keys

I have an array of objects with 3 keys, lat, lng and value. e.g.

let arr = [
  { lat: 20, lng: 30, value: 3 },
  { lat: 25, lng: 25, value: 4 },
  { lat: 20, lng: 30, value: 6 },
  { lat: 30, lng: 40, value: -5 },
  { lat: 25, lng: 25, value: 7 },
];

I'd like to compute the sum for each lat/lng pair, expecting the following result:

let res = [
  { lat: 20, lng: 30, value: 9 },
  { lat: 25, lng: 25, value: 11 },
  { lat: 30, lng: 40, value: -5 }
];

I have tried to store the results into an intermediate object, like this:

const temp = res.reduce((acc, { lat, lng, value }) => {
  acc[lat] = acc[lat] || {};
  acc[lat][lng] = acc[lat][lng] || 0;
  acc[lat][lng] += value;
  return acc;
}, {});

so temp would look like this:

let temp = { 
  20: {
    30: 9
  },
  25: {
    25: 11
  },
  30: {
    40: -5
  }
};

And then convert it to the format I want:

let res = Object.keys(temp).reduce((acc, lat) => {
  let tt = Object.keys(temp[lat]).map(lng => {
    return {lat: lat, lng: lng, value: temp[lat][lng]};
  });
  acc.push(...tt);
  return acc;
}, []);

This looks highly inefficient and I'm sure there is a smarter way to do it. I have looked at lodash's groupBy and sumBy but I couldn't put a solution together.

Upvotes: 1

Views: 879

Answers (3)

T.J. Crowder
T.J. Crowder

Reputation: 1075705

I'd loop through and keep track of previous entries with a matching lat and lng in a Map, then grab the Map's values at the end:

const known = new Map();
for (const {lat, lng, value} of arr) {
    const key = `${lat}:${lng}`;
    const prev = known.get(key);
    if (prev) {
        prev.value += value;
    } else {
        known.set(key, {lat, lng, value});
    }
}
const result = [...known.values()];

Live Example:

let arr = [
  { lat: 20, lng: 30, value: 3 },
  { lat: 25, lng: 25, value: 4 },
  { lat: 20, lng: 30, value: 6 },
  { lat: 30, lng: 40, value: -5 },
  { lat: 25, lng: 25, value: 7 },
];

const known = new Map();
for (const {lat, lng, value} of arr) {
    const key = `${lat}:${lng}`;
    const prev = known.get(key);
    if (prev) {
        prev.value += value;
    } else {
        known.set(key, {lat, lng, value});
    }
}
const result = [...known.values()];
console.log(result);

You could do that with reduce:

let arr = [
  { lat: 20, lng: 30, value: 3 },
  { lat: 25, lng: 25, value: 4 },
  { lat: 20, lng: 30, value: 6 },
  { lat: 30, lng: 40, value: -5 },
  { lat: 25, lng: 25, value: 7 },
];

const result = [...arr.reduce((map, {lat, lng, value}) => {
    const key = `${lat}:${lng}`;
    const prev = map.get(key);
    if (prev) {
        prev.value += value;
    } else {
        map.set(key, {lat, lng, value});
    }
    return map;
}, new Map()).values()];
console.log(result);

...but it doesn't buy you anything; it's just easier to get wrong. (I'm one of many who believe that unless you're doing functional programming with a predefined, reusable set of reducer functions, reduce is just unnecessary complication.)

Upvotes: 2

TKoL
TKoL

Reputation: 13922

I would usually do something like this:

let arr = [
  { lat: 20, lng: 30, value: 3 },
  { lat: 25, lng: 25, value: 4 },
  { lat: 20, lng: 30, value: 6 },
  { lat: 30, lng: 40, value: -5 },
  { lat: 25, lng: 25, value: 7 },
];

const temp = arr.reduce((acc, { lat, lng, value }) => {
  const key = `${lat}:${lng}`;
  acc[key] = acc[key] || 0;
  acc[key] += value;
  return acc;
}, {});

console.log(temp);

Upvotes: -1

Nina Scholz
Nina Scholz

Reputation: 386868

You could take a flat object and get the values from it as result set.

An array of keys are used to get a joined key.

let array = [{ lat: 20, lng: 30, value: 3 }, { lat: 25, lng: 25, value: 4 }, { lat: 20, lng: 30, value: 6 }, { lat: 30, lng: 40, value: -5 }, { lat: 25, lng: 25, value: 7 }],
    keys = ['lat', 'lng'],
    result = Object.values(array.reduce((r, o) => {
        const key = keys.map(k => o[k]).join('|');
        if (!r[key]) r[key] = { ...o, value: 0 };
        r[key].value += o.value;
        return r;
    }, {}));

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

Upvotes: 3

Related Questions