Aggregate an object in which the value for each key is an array of objects

Viewed 553

I'm using Underscore and Lodash to group an array by date. So far it's all working nicely and i'm getting an object in which the value for each key is an array of objects, like this:

 {
    "april": [
      {
        "quantity": "200",
        "date": "05/04/2020, 23:43",
        "price": "150",
        "product": "Washing Machine",
        "provider": "LG",
        "total": 30000
      },
      {
        "quantity": "1000",
        "date": "10/04/2020, 00:35",
        "price": "800",
        "product": "Television",
        "provider": "Samsung",
        "total": 800000
      },
      {
        "quantity": "3000",
        "date": "10/04/2020, 18:02",
        "price": "2",
        "product": "Computer",
        "provider": "Sony",
        "total": 600000
      },
      {
        "quantity": "1000",
        "date": "10/04/2020, 18:03",
        "price": "300",
        "product": "Bluetooth Speaker",
        "provider": "Sony",
        "total": 300000
      }
    ],
    "march": [
      {
        "quantity": "400",
        "date": "18/03/2020, 23:47",
        "price": "230",
        "product": "Home Theatre",
        "provider": "Bose",
        "total": 92000
      }
    ],
    "february": [
      {
        "quantity": "550",
        "date": "07/02/2020, 23:52",
        "price": "300",
        "product": "Printer",
        "provider": "Epson",
        "total": 165000
      },
      {
        "quantity": "750",
        "date": "07/02/2020, 23:52",
        "price": "200",
        "product": "Television",
        "provider": "Panasonic",
        "total": 150000
      }
    ]
  } 

I want to know who is the biggest provider by total for each month (for example for April is Sony with two different purchases totalling $900,000), but i've been stuck trying to access and aggregate the data. I know there are tons of similar questions here in Stackoverflow, but surprisingly I haven't been able to find any similar question with this kind of data structure. Any help would be highly appreciated.

2 Answers

You could combine map() and reduce() to achieve what you're looking for, something like:

let data = {
  april: [{
      quantity: "200",
      date: "05/04/2020, 23:43",
      price: "150",
      product: "Washing Machine",
      provider: "LG",
      total: 30000,
    },
    {
      quantity: "1000",
      date: "10/04/2020, 00:35",
      price: "800",
      product: "Television",
      provider: "Samsung",
      total: 800000,
    },
    {
      quantity: "3000",
      date: "10/04/2020, 18:02",
      price: "2",
      product: "Computer",
      provider: "Sony",
      total: 600000,
    },
    {
      quantity: "1000",
      date: "10/04/2020, 18:03",
      price: "300",
      product: "Bluetooth Speaker",
      provider: "Sony",
      total: 300000,
    },
  ],
  march: [{
    quantity: "400",
    date: "18/03/2020, 23:47",
    price: "230",
    product: "Home Theatre",
    provider: "Bose",
    total: 92000,
  }, ],
  february: [{
      quantity: "550",
      date: "07/02/2020, 23:52",
      price: "300",
      product: "Printer",
      provider: "Epson",
      total: 165000,
    },
    {
      quantity: "750",
      date: "07/02/2020, 23:52",
      price: "200",
      product: "Television",
      provider: "Panasonic",
      total: 150000,
    },
  ],
};

let aggregatedData = Object.keys(data).map((month) =>
  data[month].reduce((acc, current, i) => {
    let existing = acc.find((o) => o.provider === current.provider);
    if (existing) {
      existing.total += current.total;
    } else {
      acc[i] = { provider: current.provider, total: current.total };
    }
    return acc;
  }, [])
);

let biggestProviders = aggregatedData.map((data) =>
  data.reduce((p, c) => (c.total > p.total ? c : p))
);

console.log(biggestProviders);
console.log(biggestProviders.map(o => o.provider));

Based on the keys of the data, we get each month's array of company data and reduce the objects, accumulating their totals if the provider already exists. Then we map and reduce the resulting data by comparing which one had the highest total, then we just map the final result to get the name of the provider.

Edge case: I've also noticed a small issue with this approach, which is in the case of there being two providers with exactly the same total, so I've made a quick snippet that will just return an array of arrays of providers that just so happen to have the same total. If we don't take that into consideration we might be returning just one of the biggest providers by total:

let data = {
  april: [{
      quantity: "200",
      date: "05/04/2020, 23:43",
      price: "150",
      product: "Washing Machine",
      provider: "LG",
      total: 30000,
    },
    {
      quantity: "1000",
      date: "10/04/2020, 00:35",
      price: "800",
      product: "Television",
      provider: "Samsung",
      total: 900000,
    },
    {
      quantity: "3000",
      date: "10/04/2020, 18:02",
      price: "2",
      product: "Computer",
      provider: "Sony",
      total: 600000,
    },
    {
      quantity: "1000",
      date: "10/04/2020, 18:03",
      price: "300",
      product: "Bluetooth Speaker",
      provider: "Sony",
      total: 300000,
    },
  ],
  march: [{
    quantity: "400",
    date: "18/03/2020, 23:47",
    price: "230",
    product: "Home Theatre",
    provider: "Bose",
    total: 92000,
  }, ],
  february: [{
      quantity: "550",
      date: "07/02/2020, 23:52",
      price: "300",
      product: "Printer",
      provider: "Epson",
      total: 165000,
    },
    {
      quantity: "750",
      date: "07/02/2020, 23:52",
      price: "200",
      product: "Television",
      provider: "Panasonic",
      total: 165000,
    },
  ],
};

let aggregatedData = Object.keys(data).map((month) =>
  data[month].reduce((acc, current, i) => {
    let existing = acc.find((o) => o.provider === current.provider);
    if (existing) {
      existing.total += current.total;
    } else {
      acc[i] = {
        provider: current.provider,
        total: current.total
      };
    }
    return acc;
  }, [])
);

let biggestProviders = aggregatedData.map((data) =>
  data.reduce((p, c) => {
    if (p.length === 0) return p.concat(c);

    if (c.total === p[0].total) {
      p.push(c);
    } else {
      p = c.total > p[0].total ? Array.of(c) : Array.of(...p);
    }
    return p;
  }, [])
);

console.log(biggestProviders);

You can use a temporary object (resultObj below) to accumulate the total for each provider per month as you iterate through the list.

Sample of an object for keeping track of max total price per month:

var resultObj = {
    "max" : {
        "num" : 0,          // current max total goes here
        "provider" : ""     // provider with max total goes here
    },
    "maxObject" : {},       // max items for each month stored here
    getMax : function() {
        // returns current max object to be added to maxObject[month]
        return { "provider": this.max.provider,  "total": this.max.num };    
    },
    "reset" : function() {      // reset max
        this.max.num = 0;
        this.max.provider = ""
    },
    "createKey" : function(month) {     // create month key
        if(!this.hasOwnProperty(month))
            this[month] = {};
    }
};

And the function to iterate through the array of objects:

function getMaxValue() {
  for(let month in obj) {               // for each month in the object
    resultObj.reset();                  // reset max
    resultObj.createKey(month);         // create key for month in resultObj
    obj[month].forEach(function(el) {   // for each object within month
      if(resultObj[month][el.provider]) {      // if the provider exists as a key
        resultObj[month][el.provider] += el.total;
      } else {                                 // provider doesn't yet exist
        resultObj[month][el.provider] = el.total;
      }

      // if current total is greater than current max (for given month)
      if(resultObj[month][el.provider] > resultObj.max.num) {
        resultObj.max.num = resultObj[month][el.provider];
        resultObj.max.provider = el.provider;
      }
    });
    resultObj.maxObject[month] = resultObj.getMax();  // generate result for month
 }
    return resultObj.maxObject;    // return the result object
}

Check and test below:

var obj = {
    "april": [
    {
      "quantity": "200",
      "date": "05/04/2020, 23:43",
      "price": "150",
      "product": "Washing Machine",
      "provider": "LG",
      "total": 30000
    },
    {
      "quantity": "1000",
      "date": "10/04/2020, 00:35",
      "price": "800",
      "product": "Television",
      "provider": "Samsung",
      "total": 800000
    },
    {
      "quantity": "3000",
      "date": "10/04/2020, 18:02",
      "price": "2",
      "product": "Computer",
      "provider": "Sony",
      "total": 600000
    },
    {
      "quantity": "1000",
      "date": "10/04/2020, 18:03",
      "price": "300",
      "product": "Bluetooth Speaker",
      "provider": "Sony",
      "total": 300000
    }
  ],
  "march": [
    {
      "quantity": "400",
      "date": "18/03/2020, 23:47",
      "price": "230",
      "product": "Home Theatre",
      "provider": "Bose",
      "total": 92000
    }
  ],
  "february": [
    {
      "quantity": "550",
      "date": "07/02/2020, 23:52",
      "price": "300",
      "product": "Printer",
      "provider": "Epson",
      "total": 165000
    },
    {
      "quantity": "750",
      "date": "07/02/2020, 23:52",
      "price": "200",
      "product": "Television",
      "provider": "Panasonic",
      "total": 150000
    }
   ]
};
  
var resultObj = {
  "max" : {
    "num" : 0,
    "provider" : ""
  },
  "maxObject" : {},
  "getMax" : function() {
    return { "provider": this.max.provider,  "total": this.max.num };
  },
  "reset" : function() {
      this.max.num = 0;
      this.max.provider = "";
  },
  "createKey" : function(month) {
    if(!this.hasOwnProperty(month))
      this[month] = {};
  }
};
  
function getMaxValue() {
  for(let month in obj) {
    resultObj.reset();
    resultObj.createKey(month);
    obj[month].forEach(function(el) {
      if(resultObj[month][el.provider]) {
        resultObj[month][el.provider] += el.total;
      } else {
        resultObj[month][el.provider] = el.total;
      }

      if(resultObj[month][el.provider] > resultObj.max.num) {
        resultObj.max.num = resultObj[month][el.provider];
        resultObj.max.provider = el.provider;
      }
    });
    resultObj.maxObject[month] = resultObj.getMax();
 }
    return resultObj.maxObject;
}

var maxValues = getMaxValue();     // receives an object
console.log( maxValues );          // prints the all the results
console.log("\nmax for April:");    
console.log( maxValues.april );      // prints results for April

console.log("\nNow to check resultObj:");
console.log( resultObj );          // prints the whole temp object

Related