Search code examples
javascriptarraysobjectlodash

How to aggregate object values by key in another object


I have two arrays of objects like below:

array1 = [
   { material: "ABC123", cost: 100 },
   { material: "DEF456", cost: 150 }
]

array2 = [
  { material: "ABC123", date: "1/1/20", quantity: 4 },
  { material: "ABC123", date: "1/15/20", quantity: 1 },
  { material: "ABC123", date: "2/15/20", quantity: 3 },
  { material: "ABC123", date: "4/15/21", quantity: 1 },
  { material: "DEF456", date: "3/05/20", quantity: 6 },
  { material: "DEF456", date: "3/18/20", quantity: 1 },
  { material: "DEF456", date: "5/15/21", quantity: 2 }
]

I'd like to create a new array of objects that includes all key/value pairs from array1 along with the aggregated quantity for each item by year and month.

The result would be:

[
ABC123: {
    cost: 100,
    byYear: {
        2020: {
            byMonth: { 
                1: 5,
                2: 3
            }
        }
    },
    {
        2021: {
            byMonth: {
                4: 1
            }
        }
    },
},
DEF456: {
    cost: 150,
    byYear: {
        2020: {
            byMonth: {
                3: 7,
                2: 3
            }
        }
    },
    {
        2021: {
            byMonth: {
                5: 2
            }
        }
    },
}
]

Below is my code so far, using lodash but that's not required in the solution. The issue I'm encountering is that the year and month keys created under each item aren't specific to that item. Each item is getting year and month keys that exist for all items.

let itemSummary = {};

 _.forEach(array1, function (item) {
    itemSummary[item["material"]] = itemSummary[item["material"]] || {}; // create material as key
    itemSummary[item["material"]] = item; // add props from array1 under each key

    _.forEach(array2, function (trans) {
      // iterate through transactions and aggregate by year and month
      let transactionYear = new Date(trans["date"]).getFullYear();
      let transactionMonth = new Date(trans["date"]).getMonth() + 1;
      itemSummary[item["material"]]["byYear"] = itemSummary[item["material"]]["byYear"] || {}; //create year key
      itemSummary[item["material"]]["byYear"][transactionYear] = itemSummary[item["material"]]["byYear"][transactionYear] || {}; // set year key

      itemSummary[item["material"]]["byYear"][transactionYear]["byMonth"] = itemSummary[item["material"]]["byYear"][transactionYear]["byMonth"] || {};
      itemSummary[item["material"]]["byYear"][transactionYear]["byMonth"][transactionMonth] =
        itemSummary[item["material"]]["byYear"][transactionYear]["byMonth"][transactionMonth] || {};
    });
  });

Obviously, this doesn't aggregate the quantities as I mentioned above as I first need to get the correct year and month keys for each item.

Any assistance is much appreciated


Solution

  • You could take a dynamic approach for all groupings and another object for getting a final sum.

    const
        array1 = [{ material: "ABC123", cost: 100 }, { material: "DEF456", cost: 150 }],
        array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 }, { material: "ABC123", date: "1/15/20", quantity: 1 }, { material: "ABC123", date: "2/15/20", quantity: 3 }, { material: "ABC123", date: "4/15/21", quantity: 1 }, { material: "DEF456", date: "3/05/20", quantity: 6 }, { material: "DEF456", date: "3/18/20", quantity: 1 }, { material: "DEF456", date: "5/15/21", quantity: 2 }],
        groups = [
            ({ material }) => material,
            _ => 'byYear',
            ({ date }) => '20' + date.split('/')[2],
            _ => 'byMonth'
        ],
        sum = {
            key: ({ date }) => date.split('/')[0],
            value: ({ quantity }) => quantity
        }, 
        result = array2.reduce(
            (r, o) => {
                const temp = groups.reduce((t, fn) => t[fn(o)] ??= {}, r);
                temp[sum.key(o)] ??= 0;
                temp[sum.key(o)] += sum.value(o);
                return r;
            },
            Object.fromEntries(array1.map(({ material, cost }) => [material, { cost }]))
        );
    
    console.log(result);
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    If necessary, you could add further grouping and add an array of function for ultimate aggregation.

    const
        array1 = [{ material: "ABC123", cost: 100 }, { material: "DEF456", cost: 150 }],
        array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 }, { material: "ABC123", date: "1/15/20", quantity: 1 }, { material: "ABC123", date: "2/15/20", quantity: 3 }, { material: "ABC123", date: "4/15/21", quantity: 1 }, { material: "DEF456", date: "3/05/20", quantity: 6 }, { material: "DEF456", date: "3/18/20", quantity: 1 }, { material: "DEF456", date: "5/15/21", quantity: 2 }],
        groups = [
            ({ material }) => material,
            _ => 'byYear',
            ({ date }) => '20' + date.split('/')[2],
            _ => 'byMonth',
            ({ date }) => date.split('/')[0]
        ],
        ultimately = [
            (target, { quantity }) => {
                target.sum ??= 0;
                target.sum += quantity;
            },
            (target, { quantity }) => {
                target.max ??= quantity;
                if (quantity > target.max) target.max = quantity;
            }
        ],
        result = array2.reduce(
            (r, o) => {
                const temp = groups.reduce((t, fn) => t[fn(o)] ??= {}, r);
                ultimately.forEach(fn => fn(temp, o));
                return r;
            },
            Object.fromEntries(array1.map(({ material, cost }) => [material, { cost }]))
        );
    
    console.log(result);
    .as-console-wrapper { max-height: 100% !important; top: 0; }