How to combine all objects into one based on key

Here is the scenario I am looking at:

I want to reduce these objects

const data = [
  {
    id: 1,
    totalAmount: 1500,
    date: '2021-01-01',
    vendor_id: 2,
    totalTransaction: 12,
    isRefund: false,
  },
  {
    id: 2,
    totalAmount: 200,
    date: '2021-01-01',
    vendor_id: 2,
    totalTransaction: 2,
    isRefund: true,
  },
  {
    id: 3,
    totalAmount: 200,
    date: '2021-01-01',
    vendor_id: 2,
    totalTransaction: 1,
    isRefund: true,
  },
];

and I found a solution that adds their values:

const deepMergeSum = (obj1, obj2) => {
  return Object.keys(obj1).reduce((acc, key) => {
    if (typeof obj2[key] === 'object') {
      acc[key] = deepMergeSum(obj1[key], obj2[key]);
    } else if (obj2.hasOwnProperty(key) && !isNaN(parseFloat(obj2[key]))) {
      acc[key] = obj1[key] + obj2[key]
    }
    return acc;
  }, {});
};

const result = data.reduce((acc, obj) => (acc = deepMergeSum(acc, obj)));
const array = []
const newArray = [...array, result]

which results to:

const newArray = [
 {
   id: 6,
   totalAmount: 1900,
   date: '2021-01-012021-01-012021-01-01',
   vendor_id: 6,
   totalTransaction: 15
 }
]

And now my problem is I don't know yet how to work this around to have my expected output which if isRefund is true, it must be subtracted instead of being added, retain the vendor_id and also the concatenated date instead of only one entry date:

const newArray = [
 {
   id: 1(generate new id if possible),
   totalAmount: 1100,
   date: '2021-01-01',
   vendor_id: 2,
   totalTransaction: 15,
   isRefund: null(this can be removed if not applicable),
 },
];

I will accept and try to understand any better way or workaround for this. Thank you very much.


As you want custom behaviour for several fields, and don't need the recursive aspect of the merge, I would suggest you create a custom merge function, specific to your business logic:

const data = [{id: 1,totalAmount: 1500,date: '2021-01-01',vendor_id: 2,totalTransaction: 12,isRefund: false,},{id: 2,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 2,isRefund: true,},{id: 3,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 1,isRefund: true,},];

function customMerge(a, b) {
    if (a.vendor_id !== b.vendor_id || a.date !== b.date) {
        throw "Both date and vendor_id must be the same";
    }
    return {
        id: Math.max(a.id, b.id),
        totalAmount: (a.isRefund ? -a.totalAmount : a.totalAmount) 
                   + (b.isRefund ? -b.totalAmount : b.totalAmount),
        date: a.date,
        vendor_id: a.vendor_id,
        totalTransaction: a.totalTransaction + b.totalTransaction
    };
}

const result = data.reduce(customMerge);
if (data.length > 1) result.id++; // Make id unique
console.log(result);

You could also reintroduce the isRefund property in the result for when the total amount turns out to be negative (only do this when data.length > 1 as otherwise result is just the original single object in data):

result.isRefund = result.totalAmount < 0;
result.totalAmount = Math.abs(result.totalAmount);

Distinct results for different dates and/or vendors

Then use a "dictionary" (plain object or Map) keyed by date/vendor combinations, each having an aggregating object as value.

To demonstrate, I added one more object in the data that has a different date and amount of 300:

const data = [{id: 1,totalAmount: 1500,date: '2021-01-01',vendor_id: 2,totalTransaction: 12,isRefund: false,},{id: 2,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 2,isRefund: true,},{id: 3,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 1,isRefund: true,},{id: 4,totalAmount: 300,date: '2021-01-02',vendor_id: 2,totalTransaction: 1,isRefund: false,}];

function customMerge(acc, {id, date, vendor_id, totalAmount, totalTransaction, isRefund}) {
    let key = date + "_" + vendor_id;
    if (!(id <= acc.id)) acc.id = id;
    acc[key] ??= {
        date,
        vendor_id,
        totalAmount: 0,
        totalTransaction: 0
    };
    acc[key].totalAmount += isRefund ? -totalAmount : totalAmount;
    acc[key].totalTransaction += totalTransaction;
    return acc;
}

let {id, ...grouped} = data.reduce(customMerge, {});
let result = Object.values(grouped).map(item => Object.assign(item, { id: ++id }));
console.log(result);

it could be help, if you are looking for the same output, can add other checklist based on your requirement, for filtered date, logic would be little different but the output will be same.

const getTotalTranscation = () =>
  transctionLists.reduce((prev, current) => {
    const totalAmount = current.isRefund
      ? prev.totalAmount - current.totalAmount
      : prev.totalAmount + current.totalAmount;

    const totalTransaction = current.isRefund
      ? prev.totalTransaction - current.totalTransaction
      : prev.totalTransaction + current.totalTransaction;

    return {
      ...current,
      id: current.id + 1,
      totalAmount,
      totalTransaction,
      isRefund: totalAmount < 0,
    };
  });