Как объединить массив объектов с одинаковым значением свойства?

Есть массив объектов с кодом, видом услуги и суммой:

let Arr = [
{
serviceid: 1,
servicename : "Откопка ямы",
sum : 30
},
{
serviceid: 1,
servicename : "Откопка ямы",
sum : 50
},
{
serviceid: 2,
servicename : "Кладка плитки",
sum : 20
},
{
serviceid: 2,
servicename : "Кладка плитки",
sum : 25
},
{
serviceid: 3,
servicename : "Услуги трактора",
sum : 44
}
]

Видов услуг может быть много. Необходимо посчитать сумму с одинаковыми serviceid и на выходе получить объединенный массив объектов:

[
    {
    serviceid: 1,
    servicename : "Откопка ямы",
    sum : 80
    },
    {
    serviceid: 2,
    servicename : "Кладка плитки",
    sum : 45
    },
    {
    serviceid: 3,
    servicename : "Услуги трактора",
    sum : 44
    }
    ]

Ответы (4 шт):

Автор решения: SwaD

Могу предложить 2 варианта для суммирования элементов массива и возврата нового массива с агрегированными элементами:

Вариант для больших массивов

let Arr = [
  {
    serviceid: 1,
    servicename : "Откопка ямы",
    sum : 30
  },
  {
    serviceid: 1,
    servicename : "Откопка ямы",
    sum : 50
  },
  {
    serviceid: 2,
    servicename : "Кладка плитки",
    sum : 20
  },
  {
    serviceid: 2,
    servicename : "Кладка плитки",
    sum : 25
  },
  {
    serviceid: 3,
    servicename : "Услуги трактора",
    sum : 44
  }
];

function calcArr(arr) {
  const res = [];
  const resSum = {}
  arr.forEach(item => {
    if (resSum[item.serviceid]) {
      resSum[item.serviceid].sum += item.sum;
    } else {
      resSum[item.serviceid] = {
        sum: item.sum,
        servicename: item.servicename
      }
    }
  });
  for (let x in resSum) {
    res.push({
      serviceid: +x,
      servicename: resSum[x].servicename,
      sum: resSum[x].sum
    })
  }
  return res;
}

console.log(calcArr(Arr));

Вариант для маленьких массивов

let Arr = [
  {
    serviceid: 1,
    servicename : "Откопка ямы",
    sum : 30
  },
  {
    serviceid: 1,
    servicename : "Откопка ямы",
    sum : 50
  },
  {
    serviceid: 2,
    servicename : "Кладка плитки",
    sum : 20
  },
  {
    serviceid: 2,
    servicename : "Кладка плитки",
    sum : 25
  },
  {
    serviceid: 3,
    servicename : "Услуги трактора",
    sum : 44
  }
];

function reduceCalc(arr) {
  return arr.reduce((acc, item) => {
    const pos = acc.findIndex(it => it.serviceid === item.serviceid);
    if (pos === -1) acc.push(item)
    else acc[pos].sum += item.sum;
    return acc;
  }, [])
}

console.log(reduceCalc(Arr));

Времы выполнения:

Элементов Повторений Время forEach+for Время reduce
5 10000 11.187ms 3.74ms
5 100000 63.639ms 8.433ms
1000 10000 88.678ms 95.673ms
1000 100000 862.047ms 1.001s
10000 10000 761.753ms 1.631s
10000 100000 7.529s 16.435s
100000 10000 7.563s 17.191s
100000 100000 1:14.519 (m:ss.mmm) 3:02.386 (m:ss.mmm)
→ Ссылка
Автор решения: EzioMercer

Можно сделать так:

  1. Создаём словарь - new Map()
  2. Проходимся по элемнтам массива (reduce) и смотрим, если в словаре есть объект с данным serviceid, то достаём его и добавляем к сумме значение sum - curr.sum += obj.sum
  3. Если нет, то добавлем новый элемент с суммой равной нулю - groups.set(obj.serviceid, {...obj, sum: 0})
  4. Далее достаём из словаря все значения (.values()) и преобразуем их в массив с помощью синатксиса [...]

const arr = [{
    serviceid: 1,
    servicename: "Откопка ямы",
    sum: 30
  },
  {
    serviceid: 1,
    servicename: "Откопка ямы",
    sum: 50
  },
  {
    serviceid: 2,
    servicename: "Кладка плитки",
    sum: 20
  },
  {
    serviceid: 2,
    servicename: "Кладка плитки",
    sum: 25
  },
  {
    serviceid: 3,
    servicename: "Услуги трактора",
    sum: 44
  }
];

const groupArr = [...arr.reduce((groups, obj) => {  
  if (!groups.has(obj.serviceid)) groups.set(obj.serviceid, {...obj, sum: 0});
  
  const curr = groups.get(obj.serviceid);
  
  curr.sum += obj.sum;
  
  return groups;  
}, new Map()).values()];

console.log(groupArr);

→ Ссылка
Автор решения: Eugene X

Кину и свой камень в огород, видимо господа программисты не знают что find возвращает по референсу, и можно запросто менять объект прям в внутри массива.

let result = Arr.reduce((out, el, index, inp) => {
    /** где
     out - в первом шаге это 2й параметр функции reduce,в последующих то что возвращает return
     el - текущий элемент итерации
     index -  порядковое число текущего элемента el
     inp - сам массив целиком
    */
    let last = out.find(e => e.serviceid === el.serviceid)
    if (last) {
        last.sum += el.sum
    } else {
        out.push(el)
    }
    return out
}, [])

Задающему вопрос хочу сказать, что-бы вас не минусовали и вопрос был уместен. Во первых пожалуйста изучите хотя-бы стандарты написания кода JS

  • Google JavaScript Style Guide
  • JSCS

Ваш код выглядит ужасно!

Во вторых задавайте вопрос по существу - не понимаю как работает reducer, поясните пожалуйста на моём примере

→ Ссылка
Автор решения: ksa

Предложу еще такой вариант... Всего один проход по массиву. Далее применяется объектный метод.

let Arr = [
  {
  serviceid: 1,
  servicename : "Откопка ямы",
  sum : 30
  },
  {
  serviceid: 1,
  servicename : "Откопка ямы",
  sum : 50
  },
  {
  serviceid: 2,
  servicename : "Кладка плитки",
  sum : 20
  },
  {
  serviceid: 2,
  servicename : "Кладка плитки",
  sum : 25
  },
  {
  serviceid: 3,
  servicename : "Услуги трактора",
  sum : 44
  }
]
console.log(test(Arr))
//
function test(arr) {
  const obj = arr.reduce((obj, o) => {
    if (obj[o.serviceid]) obj[o.serviceid].sum += o.sum 
    else obj[o.serviceid] = {...o}
    return obj
  }, {})
  return Object.values(obj)
}

→ Ссылка