Search code examples
javascriptarraysobjectlodash

How to aggregate nested values in array of objects


I have an existing function that creates a summary object from two arrays (one array is transaction data, the other is master data).

Currently, the returned object contains a sum of the transaction quantity by year and month - example below.

I'm looking for help to add a sum of sums at the root level for each item - example also below.

I've reviewed answers for similar questions but wasn't able to find a solution close enough to my use case.

Here's the existing code:

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 mentioned above, I would like it to add a "totalSum" key/value at the root level. The result would look like:

{
"ABC123": {
    "totalSum": 8,
    "cost": 100,
    "byYear": {
        "2020": {
            "byMonth": {
                "1": {
                    "sum": 5,
                    "max": 4
                },
                "2": {
                    "sum": 3,
                    "max": 3
                    }
                }
            }
        }
    }
}

Any help is greatly appreciated!


Solution

  • You can actually achieve the entire refactoring with a single Array#reduce() call, combined with a Map of the master array to retrieve cost.

    Adding a total sum is then trivial since you have the all the properties to hand and don't have to remap your aggregated data.

    const
      array1 = [{ material: "ABC123", cost: 100, critical: true }, { material: "DEF456", cost: 150, critical: false }],
      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 }],
    
      master = new Map(array1.map(({ material, ...item }) => [material, item])),
      result = array2.reduce((acc, { material, date, quantity }) => {
        let [month, , year] = date.split('/');
        year = '20' + year;
    
        const
          _material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),
          _byYear = (_material.byYear[year] ??= { byMonth: {} }),
          _byMonth = (_byYear.byMonth[month] ??= { sum: 0, max: 0 });
    
        _material.totalSum += quantity;
        _byMonth.sum += quantity;
        _byMonth.max = Math.max(_byMonth.max, quantity);
    
        return acc;
      }, {});
    
    console.log(result);
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    Edit

    In a comment you mentioned that you might have more properties in array1 that should be included in the final result. I've edited to accommodate this by changing the Map to reference the entire object as found in array1. We can then include all the properties available using spread syntax when we first declare the relevant object in the accumulator.

    // split out 'material' from the rest of the object in the Map
    const master = new Map(array1.map(({ material, ...item }) => [material, item]));
    
    // later access the object by material and spread its props into the accumulator object.
    _material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),