Array of objects - Merge objects by name and sum their counts

Viewed 86

I'm stucked and need help. I have an object and need to merge objects by name and sum their counts.

products = {
    fruit: [
        { name: orange, count: 1 },
        { name: orange, count: 2 },
        { name: apple, count: 3 },
        { name: apple, count: 2 },
    ],
    vegetables: [
        { name: tomato, count: 3 },
        { name: tomato, count: 4 },
        { name: onion, count: 1 },
        { name: onion, count: 3 },
    ]
}

Final goal is to get object like this

products = {
    fruit: [
        { name: orange, count: 3 },
        { name: apple, count: 5 },
    ],
    vegetables: [
        { name: tomato, count: 7 },
        { name: onion, count: 4 },
    ]
}

Using for in loop I got into products object. I tried with reduce function but didn't succeed. Also I tried with [... new Set()] and I managed to merge objects by name, but counts didn't sum their values.

let temp = {}
for(let category in products){
    const product = products[category]
    temp = product.reduce((acc, curr) => {
        if(acc.name === curr.name){
            return{
                name: acc.name,
                count: acc.count + curr.count
            }
        }
    }, {})
}
3 Answers

You can use grouping by hash approach:

const products={fruit:[{name:'orange',count:1},{name:'orange',count:2},{name:'apple',count:3},{name:'apple',count:2}],vegetables:[{name:'tomato',count:3},{name:'tomato',count:4},{name:'onion',count:1},{name:'onion',count:3}]};

const result = Object.keys(products).reduce((prods, key) => {
    const items = products[key];
    const grouped = items.reduce((acc, { name, count }) => {
        acc[name] ??= { name, count: 0 };
        acc[name].count += count;
        return acc;
    }, {});
    prods[key] = Object.values(grouped);
    
    return prods;
}, {});

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

The below may be one possible implementation to obtain the desired objective.

Code Snippet

// helper method to manipulate 
// the fruit, vegetables arrays
const arrReduce = arr => (
  Object.entries(         // extract key-value pairs from below ".reduce()" result
    arr.reduce(           // iterate over the array with "acc" as accumulator
      (acc, {name, count}) => ({
        ...acc,           // retain existing "acc" entries
        [name]: (acc?.[name] ?? 0) + count
      }),                 // accumulate the "count" as-per "name"
      {}                  // initial "acc" is an empty-object 
    )
  ).map(
    ([k, v]) => ({        // transform the key-value pairs into objects
      name: k, count: v   // "name" and "count" props with corresponding values
    })
  )
);

// the method that does the transformation
const groupAndSum = obj => (
  Object.fromEntries(     // re-construct object from below result
    Object.entries(obj)   // transform key-value pairs into [k, v] array
    .map(                 // iterate over the array created
      ([k, v]) => ([      // use the "arrReduce()" helper method on the value-array
        k, arrReduce(v)
      ])
    )
  )
);


const products = {
  fruit: [{
    name: 'orange',
    count: 1
  }, {
    name: 'orange',
    count: 2
  }, {
    name: 'apple',
    count: 3
  }, {
    name: 'apple',
    count: 2
  }],
  vegetables: [{
    name: 'tomato',
    count: 3
  }, {
    name: 'tomato',
    count: 4
  }, {
    name: 'onion',
    count: 1
  }, {
    name: 'onion',
    count: 3
  }]
};


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

Explanation

Inline comments describe the various steps

References

This solution employs the following JavaScript features

  1. Object.fromEntries()
  2. Object.entries()
  3. Array.map()
  4. Arrow functions
  5. Array.reduce()
  6. De-structuring
  7. ... spread operator (to effect a shallow-copy)
  8. ?. optional chaining
  9. ?? nullish coalescing operator

EDIT

As noted in comments by Sebastian Simon, this answer does not provide any feedback on OP's attempt. Consequently, this edit is added here. Thank you Sebastian Simon for your feedback. Appreciate it.

Fixing OP's code

const products={fruit:[{name:'orange',count:1},{name:'orange',count:2},{name:'apple',count:3},{name:'apple',count:2}],vegetables:[{name:'tomato',count:3},{name:'tomato',count:4},{name:'onion',count:1},{name:'onion',count:3}]};

let temp = {}
for(let category in products){
    const product = products[category]
    temp = {
      ...temp,      // one needs to append results to "temp"
      [category]: Object.entries( // extract key-value pairs from result of ".reduce()"
        product
          .reduce(    // the ".reduce()" result needs to go within "category"
            (acc, curr) => {
            return {  // no need of "if" here. Always return an object
              ...acc, // retain existing "accumulator"
              [curr.name]: (    // add / update key "curr.name" of "acc" object
                acc
                ?.[curr.name]   // use existing "count" if "name" exists in "acc"
                ?? 0            // nullish coalescing to zero (if "name" not in "acc")
              ) + curr.count    // always add "curr.count" to the value
            };
          },
          {}
        )
      ).map(      // transform the key-value pair into the desired "name", "count" object
        ([k, v]) => ({
          name: k, count: v
        })
      )
    }
};

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

Explanation

The changes made along with description is added to the above code-snippet.

Use Array#map, Object.fromEntries and Object.entries methods as follows:

const input = { fruit: [ { name: "orange", count: 1 }, { name: "orange", count: 2 }, { name: "apple", count: 3 }, { name: "apple", count: 2 } ], vegetables: [{ name: "tomato", count: 3 },{ name: "tomato", count: 4 },{ name: "onion", count: 1 }, { name: "onion", count: 3 }]};

const output =  Object.fromEntries( Object.entries(input)
    .map(([category, produce]) =>
        [
            category,
            Object.entries(
                produce.reduce((prev,{name,count}) =>
                    ({...prev,[name]:(prev[name] || 0) + count}), {}
                )
            )
            .map(([name, count]) => ({name,count}))
        ]
    )
);

console.log( output );

YOUR CODE

You could also edit your code as follows; you have first have to look in acc to see if a matching item (by name) exists - Array#find. If found, update the count, otherwise push curr into acc. BE SURE TO RETURN acc! The initial value should be an empty array, [].

const products = { fruit: [ { name: "orange", count: 1 }, { name: "orange", count: 2 }, { name: "apple", count: 3 }, { name: "apple", count: 2 } ], vegetables: [{ name: "tomato", count: 3 },{ name: "tomato", count: 4 },{ name: "onion", count: 1 }, { name: "onion", count: 3 }]};

const output = {}
for(let category in products) {
    const product = products[category]
    output[category] = product.reduce((acc, curr) => {
        let target = acc.find(item => item.name === curr.name);
        if( target ) {
            target.count += curr.count;
        } else {
            acc.push( curr );
        }
        return acc;
    }, [])
};

console.log( output );

Related