1

I have a function that returns 2 separate arrays containing 2 arrays.

[Array(3), Array(3)]

[Array(3), Array(3)]

Those nested arrays each contain 6 objects

{value: "10.50", category: "a"}, 
{value: "55.50", category: "a"}
{value: "10.50", category: "b"}, 
{value: "35.50", category: "b"},
{value: "15.50", category: "c"},
{value: "45.50", category: "c"}

I am trying to iterate through each main 2 arrays and return the summed values of the nested arrays by category like below: Keeping in mind the value is currently a string.

[
 [
  {value: "66.00", category: "a"},
  {value: "46.00", category: "b"},
  {value: "61.00", category: "c"},
 ]
]

I have tried the below but I cant seem to get it right, I have looked at numerous SO threads and I know there are tons of similar ones but nothing is working for me so far. TIA.

my poor attempt.

  const x = array.forEach((data) => {
    data.reduce(function (
      acc: any[],
      val: { category: string; value: string }
    ) {
      const o = acc
        .filter(function (obj) {
          return obj.category === val.category;
        })
        .pop() || { category: val.category, value: 0 };

      o.value += parseFloat(String(val.value));
      acc.push(o);
      return acc;
    },
    []);
  });
0

3 Answers 3

2

Sounds like you just need to group into an object indexed by category, adding up the values, making sure to coerce the strings to numbers, then turn it back into an array of objects afterwards:

const outerArr = [
  [
    {value: "10.50", category: "a"}, 
    {value: "55.50", category: "a"},
    {value: "10.50", category: "b"}, 
    {value: "35.50", category: "b"},
    {value: "15.50", category: "c"},
    {value: "45.50", category: "c"}
  ],
  // etc
];

const final = outerArr.map((subarr) => {
  const grouped = {};
  for (const { value, category } of subarr) {
    grouped[category] = (grouped[category] || 0) + Number(value);
  }
  return Object.entries(grouped).map(([category, value]) => ({ value: value.toFixed(2), category }));
});
console.log(final);

Sign up to request clarification or add additional context in comments.

4 Comments

Dude, you certainly are a rockstar ;) Thanks for the answer. Do you have any references or pointers on where I can practice and get good at this sort of stuff, besides the usual?
Seeing what questions come across here is one way. Eg stackoverflow.com/…
Great, I will definitely try answer a few to give back. Shot in the dark, would you know the Typescript syntax for the above?
TS has slight problems with dynamic keys in object literals. I'd use a Map instead, typed as <string, number>
2

The final value is in string format, though you can change as per requirement. Example for single array:

Object.entries(
  arr.reduce((acc,curr)=>{
    acc[curr.category] = (acc[curr.category] || 0.0) + parseFloat(curr.value)
    return acc;
  },{})
)
.map(([a,b])=>({category:a,value:b.toFixed(2).toString()}));

1 Comment

Nicely done. I think .toFixed(2).toString() might be redundant though. toFixed() already returns a string.
1

There are several other good answers to your question, though I'd argue that this one is shorter, faster, and simpler to understand. (Look at the bottom of my solution for the working example). We can achieve this using a few simple array and object methods.

1. Consolidating the data

First, we'll consolidate each of your nested arrays of objects into single objects, with the categories (our unique values) as the keys, and their values. We can do this using the array reduce() method. Here we'll be using {} as our accumulator's initial value and then push to that accumulator object by setting its properties. As we iterate through, we'll add each value to the associated category property. This can cause errors if the property is not yet set on the accumulator object.

One way of working around this would be to use a ternary to check whether the property exists yet, and then either set the property if one doesn't yet exist, or add to the value if it does, like this:

// a is the accumulator, c is our category name, and v is our value
a[c] ? a[c] += parseFloat(v) : a[c] = parseFloat(v) // -> {a: 66, b: 46, c: 61}

A simpler way to do this is with nullish coalescing. Nullish coalescing will check to see if the value is null or undefined and if so, we use 0 as the current value. In this method, we are always setting property, and just overwriting it with itself (or 0) added to the current value being evaluated, like this:

a[c] = (a[c] ?? 0) + parseFloat(v) // -> {a: 66, b: 46, c: 61}

I also opted to use object restructuring when setting the parameter names in my reduce method to have all three variables easily accessible to me. This is not required though, so either of the below methods would work just the same. Using object destructuring also allows us to rename our destructured parameters to easier use (c, v instead of category, value). To learn more about destructuring and renaming parameters, check out Wes Bos's post, Rename & Destructure Variables in ES6.

// Without Object Destructuring
arr.reduce((a, o) => (a[o.category] = (a[o.category] ?? 0) + parseFloat(o.value), a), {});

// With Object Destructuring
arr.reduce((a, {value: v, category: c}) => (a[c] = (a[c] ?? 0) + parseFloat(v), a), {});

2. Expanding the consolidated object(s) into an array of objects

Once we have our simplified object (e.g. {a: 66, b: 46, c: 61}) for each array of objects, our next task is to transform it back into the series of objects format we started with, but now consolidated. To do this, we first convert our new object of key-value pairs into an array of nested arrays using the Object.entries() method. This will allow us to re-map the array as an array of objects, during which we'll rewrite our desired key names and convert our numbered values back into strings with a fixed decimal count of 2.

In an earlier step, we destructured an object using its property names. This time, because Object.entries() returns our data as an array, we'll be using array destructuring. This is similar to object destructuring, except that we'll need to use brackets when we restructure our array. Remember to always wrap your destructured parameters in parentheses or you'll get an error:

// obj = {a: 66, b: 46, c: 61};

Object.entries(obj).map([c, v] => ({ value: v.toFixed(2), category: c }))
// !!! Uncaught SyntaxError: Malformed arrow function parameter list

Object.entries(obj).map(([c, v]) => ({ value: v.toFixed(2), category: c }))
/* -> [
        {value: "66.00", category: "a"},
        {value: "46.00", category: "b"},
        {value: "61.00", category: "c"}
      ]
*/

3. Putting it all together. The final result:

Now all that's left is to put it all together! Here's the final result:

const data = [
    [
        {value: "10.50", category: "a"},
        {value: "55.50", category: "a"},
        {value: "10.50", category: "b"},
        {value: "35.50", category: "b"},
        {value: "15.50", category: "c"},
        {value: "45.50", category: "c"}
    ],
  // [ ... ]
];

const consolidate = arr => arr.map(e => Object.entries(e.reduce((a, {value: v, category: c}) => (a[c] = (a[c] ?? 0) + parseFloat(v), a), {})).map(([c, v]) => ({ value: v.toFixed(2), category: c })));

console.log(consolidate(data));

2 Comments

This was a great answer with fantastic explinations along the way! Thanks so much!
@PengProgrammer thanks so much! Glad it helped. Let me know if there’s anything I can do to optimize or improve my answer

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.