0
\$\begingroup\$

I have this function that should take some array of objects and sum up the values of the inner objects based on some keys, and conditionally add to it some values from another object.

Is there a way to optimize this code?

 function sumUpKeys(objects, totals = undefined) {
        const totalsSum = totals?.reduce((totalsAcc, currentTotalsItem) => {
            return {
                key1:
                    (totalsAcc.key1 || 0) +
                    (this.computeVal(
                        currentTotalsItem.num || 0,
                        currentTotalsItem.gross
                    ) || 0),
                key2:
                    (totalsAcc.key2 || 0) +
                    (this.computeVal(
                        currentTotalsItem.num || 0,
                        currentTotalsItem.net
                    ) || 0),
            };
        }, {});

        const objectsSum = objects.reduce((objectsAcc, currentObject) => {
            const itemsSum = currentObject.items.reduce((itemsAcc, currentItem) => {
                return {
                    key1:
                        (this.computeVal(currentObject.num, currentItem.key1) || 0) +
                         (itemsAcc.key1 || 0),
                    key2:
                        (this.computeVal(currentObject.num, currentItem.key2) || 0) + (
                        itemsAcc.key2 || 0),
                };
            }, {});
            objectsAcc = {
                key1: (objectsAcc.key1 || 0) + (itemsSum.key1 || 0),
                key2: (objectsAcc.key2 || 0) + (itemsSum.key2 || 0),
            };
            return objectsAcc;
        }, {});
        // add additional services sum to the monthly charges sum
        return {
            key1: (objectsSum.key1 || 0) + (totalsSum?.key1 || 0),
            key2: (objectsSum.key2 || 0) + (totalsSum?.key2 || 0),
        };
    }

function computeVal(num, val) {
        return val ? val * num : undefined;
    }
  
 
 const objects1 = [
    {
        num: 3,
        items: [
            {
                key1: 4,
                key2: 2,
            },
            {
                key1: 10,
                key2: 20,
            },
        ],
        key3: 'aa',
    },

    {
        num: 4,
        items: [
            {
                key1: 4,
                key2: 2,
            },
        ],
        key3: 'aa',
    },
];

const totals1 = [
    {
        num: 2,
        gross: 10,
        net: 5,
    },
    {
        num: 3,
        gross: 6,
        net: 12,
    },
];

// result with totals
const result1 = sumUpKeys(objects1, totals1);
// result without totals
const result2 = sumUpKeys(objects1);
console.log(result1);
console.log(result2);

\$\endgroup\$
2
  • \$\begingroup\$ Can you explain what you want to achieve by 'optimising'. Do you mean to run faster, use less memory, be easier to 'read', some combination, something else? \$\endgroup\$ May 12 at 9:17
  • \$\begingroup\$ @DaveMeehan yes, maybe somehow avoid the nested reduce if it's possible at all? Or have fewer lines of code. \$\endgroup\$
    – callback
    May 12 at 9:37

3 Answers 3

1
\$\begingroup\$

You are returning a new object in each iteration inside the reducers, when you could use just one object for the tallies (see below). The handling of undefined values suggest to me that the underlying data might be invalid, in which case it makes more sense to fix the data and establish validation before insert. Unless of course the values are spec'd to be optional.

const sumKeys = (xs, ys = []) => {
  const sum = {key1: 0, key2: 0}

  for (const obj of xs) {
    for (const item of obj.items) {
      sum.key1 += item.key1 * obj.num
      sum.key2 += item.key2 * obj.num
    }
  }

  for (const obj of ys) {
    sum.key1 += obj.gross * obj.num
    sum.key2 += obj.net * obj.num
  }

  return sum
}
\$\endgroup\$
1
\$\begingroup\$

I'm going to make an up-front assumption that many of the truthy checks you're doing (e.g. ... || 0) is simply to check if a value is undefined or not, and you don't actually expect other falsey values in your parameters. Using this assumption, I'll freely replace much of the || 0 logic with default parameters. If this assumption is wrong, you'll just have to toss the default parameters and add the falsey checks back in.

A major source of complexity here is the fact that each .reduce() is trying to update two independent values (key1 and key2). I found it to be much cleaner if I independently calculated the resulting key1 separately from the key2 value, instead of trying to calculate them both at the same time.

As an example, I changed the logic to calculate the totalSums object into this:

const totalsSum = {
    key1: totals.reduce((totalsAcc, currentTotalsItem) => (
        totalsAcc + computeVal(currentTotalsItem.num || 0, currentTotalsItem.gross)
    ), 0),
    key2: totals.reduce((totalsAcc, currentTotalsItem) => (
        totalsAcc + computeVal(currentTotalsItem.num || 0, currentTotalsItem.net)
    ), 0),
};

Notice how I have a different reduce function for each key? Splitting the code up like this let me notice other patterns that I could use to clean things up further. In the previous code snippet, I noticed that I could split up each .reduce() into two steps, a .map() step, then a .reduce() step.

const totalsSum = {
    key1: totals
        .map(({ num = 0, gross }) => computeVal(num, gross))
        .reduce((totalsAcc, x) => totalsAcc + x, 0),
    key2: totals
        .map(({ num = 0, net }) => computeVal(num, net))
        .reduce((totalsAcc, x) => totalsAcc + x, 0),
};

Those .reduce()es are now nothing more than a simple sum, so I could swap them out for a sum function.

const sum = array => array.reduce((acc, el) => acc + el, 0)

const totalsSum = {
    key1: sum(
        totals.map(({ num = 0, gross }) => computeVal(num, gross))
    ),
    key2: sum(
        totals.map(({ num = 0, net }) => computeVal(num, net))
    ),
};

I simplified the other areas of the code in a very similar fashion, and made some other simplifications beyond that, until I arrived at this fairly concise solution.

const sum = array => array.reduce((acc, el) => acc + el, 0);

const sumUpKeys = (objects, totals = []) => ({
    key1: sum([
        ...totals.map(({ num = 0, gross }) => num * gross),
        ...objects.map(({ num = 0, items }) => sum(
            items.map(({ key1 }) => num * key1)
        ))
    ]),
    key2: sum([
        ...totals.map(({ num = 0, net }) => num * net),
        ...objects.map(({ num = 0, items }) => sum(
            items.map(({ key2 }) => num * key2)
        ))
    ]),
});

From here, you'd notice there's a fair amount of repeated logic. If you think it makes sense, you could extract out a helper function that's capable of doing the sum for an individual key, then use that helper function for both keys. I opted not to do this, partly because I don't know enough about the concrete use case to know if this sort of refactor makes sense or not, but it is an option.

(In the end, I actually like @morbusg's final result better. But, I thought I would share the refactoring process I took, to hopefully give an idea of how to clean up code like this in the future).

\$\endgroup\$
0
\$\begingroup\$

I found it useful to ensure that the input data was sanitised to remove the possibility of undefined values before further processing. Much of the verbosity of the code was to do with checking for undefined when the alternate value was always 0, and that even extended to the computeVal function which became redundant.

The sample data was actually 'complete' and didn't include undefined values so I've not tested for outcomes based on imcomplete data. It might also be reasonable to replace || 0 with nullish (?? 0) and to consider if there might be strings or other types that would coerce to falsy and be converted to 0 when something else should happen.

Whilst it would be possible to refactor the nested reduce, it doesn't seem worth it as it now is easier to understand.

I also chose to abbreviate the variable names, again principally to reduce the verbosity without introducing ambiguity.

I suspect that the actual application data doesn't use opaque keys like key1 and key2, I suspect this was used to simplify the question. As a result, this might need a little more work to provide useful defaults, and generally the naming conventions might need aligning with the actual data.

function sanitiseObjects(objects) {
    return objects?.map(obj => {
        return {
            ...obj,
            num: obj.num || 0,
            items: obj.items.map(item => ({
                key1: item.key1 || 0,
                key2: item.key2 || 0,
            }))
        }
    }) || []
}

function sanitizeTotals(totals) {
    return totals?.map(total => ({
        ...total,
        num: total.num || 0,
        gross: total.gross || 0,
        net: total.net || 0,
    })) || []
}

function sumUpKeys(objects, totals = undefined) {

    const defaults = { key1: 0, key2: 0 }

    const totalsSum = sanitizeTotals(totals).reduce((acc, item) => {
        return {
            key1: acc.key1 + (item.num * item.gross),
            key2: acc.key2 + (item.num * item.net),
        };
    }, defaults);

    const objectsSum = sanitiseObjects(objects).reduce((objectsAcc, object) => {
        const itemsSum = object.items.reduce((itemsAcc, item) => {
            return {
                key1: (object.num * item.key1) + itemsAcc.key1,
                key2: (object.num * item.key2) + itemsAcc.key2,
            };
        }, defaults);
        return {
            key1: objectsAcc.key1 + itemsSum.key1,
            key2: objectsAcc.key2 + itemsSum.key2,
        };
    }, defaults);


    // add additional services sum to the monthly charges sum
    return {
        key1: objectsSum.key1 + (totalsSum?.key1 || 0),
        key2: objectsSum.key2 + (totalsSum?.key2 || 0),
    };
}

const objects1 = [
    { num: 3, items: [{ key1: 4, key2: 2, }, { key1: 10, key2: 20, }, ], key3: 'aa', },
    { num: 4, items: [{ key1: 4, key2: 2, }, ], key3: 'aa', },
];

const totals1 = [
    { num: 2, gross: 10, net: 5,},
    { num: 3, gross: 6, net: 12,},
];

// result with totals
const result1 = sumUpKeys(objects1, totals1);
// result without totals
const result2 = sumUpKeys(objects1);
console.log(result1);
console.log(result2);

\$\endgroup\$
1
  • \$\begingroup\$ Thanks a lot for all the insights! Very helpful. \$\endgroup\$
    – callback
    May 13 at 10:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.