1

Working with an object array, I am needing to update the count for any object with a type of Select (Multiple Answer).

Each object with a type of Select (Multiple Answer) contains a data object array with a comma separated value like "Overpriced,Unique,High quality". These values should be separated into their own object and be included in a new count and total (sum of all count values) for that particular data object array.

const arr = [
  {
     data: [
       {count: 7, total: 7, value: "N/A"},
     ],
     name: "item 1",
     type: "Yes/No",
  }, {
     data: [
       {count: 5, total: 7, value: "N/A"},
       {count: 2, total: 7, value: "Yellow"},
     ],
     name: "item 2",
     type: "Select (Single Answer)",
  }, {
     data: [
       {count: 5, total: 7, value: "N/A"},
       {count: 1, total: 7, value: "Overpriced,Unique,High quality"},
       {count: 1, total: 7, value: "Reliable,High quality"},
     ],
     name: "item 3",
     type: "Select (Multiple Answer)",
  },
];

Expected Result

const result = [
  {
     data: [
       {count: 7, total: 7, value: "N/A"},
     ],
     name: "item 1",
     type: "Yes/No",
  }, {
     data: [
       {count: 5, total: 7, value: "N/A"},
       {count: 2, total: 7, value: "Yellow"},
     ],
     name: "item 2",
     type: "Select (Single Answer)",
  }, {
     data: [
       {count: 5, total: 10, value: "N/A"},
       {count: 2, total: 10, value: "High quality"},
       {count: 1, total: 10, value: "Overpriced"},
       {count: 1, total: 10, value: "Unique"},
       {count: 1, total: 10, value: "Reliable"},
     ],
     name: "item 3",
     type: "Select (Multiple Answer)",
  },
];

I have started down the path of using a reduce function, but it produces an object far from the desired result:

Current Code

arr.reduce((a, c) => {
  a[c.data.value] = a[c.data.value] || { total: 0 };
  a[c.data.value].total += 1;
  return a;
}, {})

Undesired Outcome

{ undefined: { total: 4 } }
5
  • what does total means? Commented Jan 21, 2019 at 21:52
  • total would equal the sum of all count values inside each data array. Commented Jan 21, 2019 at 21:53
  • @proph3t You don't really need a total if it can be computed. Or rather you can store it but it should be based on the sum of the counts and not have duplicates. It's can easily be wrong or annoying to mess with based on how it's handled at the moment. Commented Jan 21, 2019 at 21:55
  • @SpencerWieczorek High quality has a count of 2 as it occurs twice within that particular data array. The total serves as basis for knowing the total of the count values within that particular data array after the re-count of the comma separated values. Commented Jan 21, 2019 at 22:00
  • this would take much more than a single reduce. youll need to count appearances and loop again to update them Commented Jan 21, 2019 at 22:05

3 Answers 3

3

You could collect the groups and get total with a closure.

It features a closure over total and returns an array

        data: (total => Array.from(


        ))(0)

by taking a Map for collecting the data as initialValue

             o.data.reduce(


                 new Map
            ),

and a function for mapping new objects with count, total and value.

            ([value, count]) => ({ count, total, value })

Inside of the callback of reduce, count and value are destructured and value is splitted for getting all counts to all splitted values collected in a map. At the same time, total gets an increment with the actual count. At the end, the map m is returned.

                 (m, { count, value }) => (value.split(',').forEach(
                     v => (m.set(v, (m.get(v) || 0) + count), total+= count)
                 ), m),

var data = [{ data: [{ count: 7, total: 7, value: "N/A" }], name: "item 1", type: "Yes/No" }, { data: [{ count: 5, total: 7, value: "N/A" }, { count: 2, total: 7, value: "Yellow" }], name: "item 2", type: "Select (Single Answer)" }, { data: [{ count: 5, total: 7, value: "N/A" }, { count: 1, total: 7, value: "Overpriced,Unique,High quality" }, { count: 1, total: 7, value: "Reliable,High quality" }], name: "item 3", type: "Select (Multiple Answer)" }],
    result = data.map(o => Object.assign({}, o, {
        data: (total => Array.from(
             o.data.reduce(
                 (m, { count, value }) => (value.split(',').forEach(
                     v => (m.set(v, (m.get(v) || 0) + count), total+= count)
                 ), m),
                 new Map
            ),
            ([value, count]) => ({ count, total, value })
        ))(0)
    }));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

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

3 Comments

+1 nice use of closure here. Just a question: your reduce callback has a comma inside parentheses, what does it represents?
@guijob, do you mean ), m) ? this is the return value of the reduce with a comma operator.
@NinaScholz Note: I'd recommend adding some indication of what the code is doing either above or via comments. This code isn't too easily readable.
1

Here is one way to do this with some comments to make it clear:

let arr = [{data:[{count:7,total:7,value:"N/A"}],name:"item 1",type:"Yes/No"},{data:[{count:5,total:7,value:"N/A"},{count:2,total:7,value:"Yellow"}],name:"item 2",type:"Select (Single Answer)"},{data:[{count:5,total:7,value:"N/A"},{count:1,total:7,value:"Overpriced,Unique,High quality"},{count:1,total:7,value:"Reliable,High quality"}],name:"item 3",type:"Select (Multiple Answer)"}];

arr.forEach(x => {
  //get all splitted values
  const allValues = x.data.filter(y => y.value.split(',').length > 1).reduce((a, e) => a.concat(e.value.split(',')), []);

  //remove non-splitten values from data array
  x.data = x.data.filter(y => y.value.split(',').length <= 1);

  //create new values from old
  const newData = allValues.reduce((a, y) => {
    const data = a.find(z => z.value === y);
    if (data) {
      data.count++;
      return a;
    };
    return a.concat({ count: 1, value: y });
  }, x.data)
  
  //create new total
  const sumCounters = newData.reduce((a, e) => a + e.count, 0);
  newData.forEach(e => e.total = sumCounters);

  x.data = newData;
  return x;
})

console.log(arr);

Comments

0

There have been several good answers provided since I started working on mine but I don't want to waste the time I put into it so here is my solution.

// OP's original array
const arr = [
  {
     data: [
       {count: 7, total: 7, value: "N/A"},
     ],
     name: "item 1",
     type: "Yes/No",
  }, {
     data: [
       {count: 5, total: 7, value: "N/A"},
       {count: 2, total: 7, value: "Yellow"},
     ],
     name: "item 2",
     type: "Select (Single Answer)",
  }, {
     data: [
       {count: 5, total: 7, value: "N/A"},
       {count: 1, total: 7, value: "Overpriced,Unique,High quality"},
       {count: 1, total: 7, value: "Reliable,High quality"},
     ],
     name: "item 3",
     type: "Select (Multiple Answer)",
  },
];

arr.forEach(function(select){
  // Only modify the multiple answer selects
  if(select.type == 'Select (Multiple Answer)'){
    var newTotal = 0; // calculate a new total as each item is added to the new array
    // use reduce to create a new data array for the multiple answer select
    var newDataArray = select.data.reduce(function(acc,d){
      valueArr = d['value'].split(','); // get a list of separate value strings
      valueArr.forEach(function(valStr){
        var unique = true;
        // if the new array is empty then go ahead and add the first item
        if(acc.length === 0){
          acc.push({'count':d.count,'total':d.total,'value':valStr});
          newTotal += d.count;
        } else {
          // check to see if there is already an object with the same value string
          // if there is then just update the count and set the unique flag so a new element doesn't get added later
          acc.forEach(function(obj){
            if(obj['value'] == valStr){
              obj['count'] += d.count;
              unique = false;
              newTotal += d.count;
            }
          })
          if(unique){
            acc.push({'count':d.count,'total':d.total,'value':valStr});
            newTotal += d.count;
          }      
        }          
      })
      return acc;
    }, []);
    // Update totals
    newDataArray.forEach(function(obj){
      obj.total = newTotal;
    })
    select.data = newDataArray;
  }
})
console.log(arr);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.