Luciano
Luciano

Reputation: 2196

How to group an array of objects by key and sums a nested object property

I have an array of product objects, each product contains the product price besides a nested object representing the currency used.

var products = [
{
    'id': '1',
    'currency': { id:1, name:'Dollar' },
    'price': '100'
}, {
    'id': '4',
    'currency': { 
        id:2, 
        name:'Euro',
        current_quotation: {
           date: '2020-03-02',
           quotation: 68
        } 
    },
    'price': '300'
}, {
    'id': '6',
    'currency': { id:1, name:'Dollar' },
    'price': '50'
}, {
    'id': '16',
    'currency': null,
    'price': '50'
}, {
    'id': '23',
    'currency': { id:2, name:'Euro' },
    'price': null
}
];

What I'm trying to do is to get an array with a unique object per currency with its price sum, preferably using lodash or ES6 script. What I need as a result is:

var subtotals = [
    {currency:'Dollar', total: 150},
    {currency:'Euro', total: 300}
]

I tried with many samples but no luck so far. My approach is this, but it's returning the whole product object within the array, which is so far from expected result

let subtotals = state.products.reduce((h, product) => Object.assign(h, { [product.currency?.name]:( h[product.currency?.name] || [] ).concat({currency: product.currency?.name, price: product.price}) }), {})

Result

{
    "Euro":[
        {"currency":"Euro","price":"300"}
    ],
    "Dolar":[
        {"currency":"Dolar","price":"100"},
        {"currency":"Dolar","price":"50"}
    ]
}

Please note that some products may have no currency at the begining since it value comes from a dropdown input. In that case the value can be ommited in the result.

Any help will be appreciated.

Upvotes: 0

Views: 710

Answers (3)

Nick Parsons
Nick Parsons

Reputation: 50749

Using lodash, you can group by the currency object's name property using _.groupBy(). And then map the result object to an array of objects. The object which you map to can use the key as the currency and the summed total of the grouped array (using _.sumBy()) as the total:

const products = [ { 'id': '1', 'currency': { id:1, name:'Dollar' }, 'price': '100' }, { 'id': '4', 'currency': { id:2, name:'Euro' }, 'price': '300' }, { 'id': '6', 'currency': { id:1, name:'Dollar' }, 'price': '50' }, { 'id': '16', 'currency': null, 'price': '50' }, { 'id': '23', 'currency': { id:2, name:'Euro' }, 'price': null } ];

const getSubTotals = p => _.flow(
  products => _.filter(products, o => _.has(o, p)),
  sm_prod => _.groupBy(sm_prod, p),
  gr => _.map(gr, (arr, currency) => ({currency, total: _.sumBy(arr, ({price}) => +price)}))
);

console.log(getSubTotals('currency.name')(products));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

EDIT As you have changed your question. You'll need to use some alternate code to get your desired result:

const products = [ { 'id': '1', 'currency': { id:1, name:'Dollar' }, 'price': '100' }, { 'id': '4', 'currency': { id:2, name:'Euro', current_quotation: { date: '2020-03-02', quotation: 68 } }, 'price': '300' }, { 'id': '6', 'currency': { id:1, name:'Dollar' }, 'price': '50' }, { 'id': '16', 'currency': null, 'price': '50' }, { 'id': '23', 'currency': { id:2, name:'Euro' }, 'price': null } ];

const getSubTotals = p => products => {
  const _gr = _(products).filter(o => _.has(o, p)).groupBy(p);
  const quote_map = _gr.mapValues((arr, k) => _.sumBy(arr, o => _.get(o, 'currency.current_quotation.quotation', 0)) || 1).value();
  const _totals = _gr.map((arr, currency) => ({currency, total: _.sumBy(arr, ({price}) => +price)}));
  return _totals.map(({currency, total, ...r}) => ({currency, total, quote_total: total*quote_map[currency]})).value();
}


console.log(getSubTotals('currency.name')(products));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

Upvotes: 1

twntr89
twntr89

Reputation: 21

I wrote this up in the console and it should work for your case:

var products = [
    {
        id: '1',
        currency: { id: 1, name: 'Dollar' },
        price: '100',
    },
    {
        id: '4',
        currency: { id: 2, name: 'Euro' },
        price: '300',
    },
    {
        id: '6',
        currency: { id: 1, name: 'Dollar' },
        price: '50',
    },
];

subtotals = {};

products.map(x => {
    if(x.currency !== null){
        subtotals[x.currency.name] === undefined
            ? (subtotals[x.currency.name] = parseInt(x.price))
            : (subtotals[x.currency.name] += parseInt(x.price));
    }
});

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 370759

Assuming that you want to sum up the totals and get a Euro total of 300:

Iterate over each currency.name and price, transforming the input into an object whose keys are the currency names and values are the cumulative price for that currency name found so far. At the end, map the object's entries to get an array of objects of your desired format:

var products = [
  {
      'id': '1',
      'currency': { id:1, name:'Dollar' },
      'price': '100'
  }, {
      'id': '4',
      'currency': { id:2, name:'Euro' },
      'price': '300'
  }, {
      'id': '6',
      'currency': { id:1, name:'Dollar' },
      'price': '50'
  }
];

const pricesByName = {};
for (const { currency: { name }, price } of products) {
  pricesByName[name] = (pricesByName[name] || 0) + Number(price);
}
const output = Object.entries(pricesByName)
  .map(([currency, total]) => ({ currency, total }));
console.log(output);

If the currency or price is null, ignore them using an if statement:

var products = [
{
    'id': '1',
    'currency': { id:1, name:'Dollar' },
    'price': '100'
}, {
    'id': '4',
    'currency': { id:2, name:'Euro' },
    'price': '300'
}, {
    'id': '6',
    'currency': { id:1, name:'Dollar' },
    'price': '50'
}, {
    'id': '16',
    'currency': null,
    'price': '50'
}, {
    'id': '23',
    'currency': { id:2, name:'Euro' },
    'price': null
}
];

const pricesByName = {};
for (const { currency, price } of products) {
  if (price === null || currency === null) continue;
  const { name } = currency;
  pricesByName[name] = (pricesByName[name] || 0) + Number(price);
}
const output = Object.entries(pricesByName)
  .map(([currency, total]) => ({ currency, total }));
console.log(output);

Upvotes: 2

Related Questions