1

Please see my Fiddle which includes all the code that follows.

My apologies if this question has been answered before. I found similar questions on here about grouping by property, but I did not find an example where the result was still an array of objects.

I'm starting with this data format:

const originalData = [
  {
    "groupId": 0,
    "color": "red",
    "shape": "circle"
  },
  {
    "groupId": 1,
    "color": "green",
    "shape": "square"
  },
  {
    "groupId": 1,
    "color": "orange",
    "shape": "hexagon"
  },
  {
    "groupId": 1,
    "color": "purple",
    "shape": "triangle"
  },
  {
    "groupId": 2,
    "color": "aqua",
    "shape": "diamond"
  },
  {
    "groupId": 2,
    "color": "blue",
    "shape": "trapezoid"
  }
];

And I would like to transform it into a new array of objects, grouped by groupId property value:

const desiredData = [
  {
    "groupId": 0,
    "items": [
      {
        "color": "red",
        "shape": "circle"
      }
    ]
  },
  {
    "groupId": 1,
    "items": [
      {
        "color": "green",
        "shape": "square"
      },
      {
        "color": "orange",
        "shape": "hexagon"
      },
      {
        "color": "purple",
        "shape": "triangle"
      }
    ]
  },
  {
    "groupId": 2,
    "items": [
      {
        "color": "aqua",
        "shape": "diamond"
      },
      {
        "color": "blue",
        "shape": "trapezoid"
      }
    ]
  }
];

This reduce function (which I found on MDN) is the closest I was able to come to transforming my data. My experience with transforming data in Javascript is limited, and I am not sure how to add fields (like group) during the transformation process. Also, the result is an object, not an array of objects.

const actualFormattedData = originalData.reduce((acc, obj) => {
  let key = obj['groupId'];
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(obj);
  return acc;
}, {});

Output from the reduce function:

{
  "0": [
    {
      "groupId": 0,
      "color": "red",
      "shape": "circle"
    }
  ],
  "1": [
    {
      "groupId": 1,
      "color": "green",
      "shape": "square"
    },
    {
      "groupId": 1,
      "color": "orange",
      "shape": "hexagon"
    },
    {
      "groupId": 1,
      "color": "purple",
      "shape": "triangle"
    }
  ],
  "2": [
    {
      "groupId": 2,
      "color": "aqua",
      "shape": "diamond"
    },
    {
      "groupId": 2,
      "color": "blue",
      "shape": "trapezoid"
    }
  ]
}

The ultimate goal is to map the array of objects in React. I know I can use Object.entries and array indices to achieve a similar result with actualFormattedData as-is, but it would be ideal if I could first make actualFormattedData look exactly like desiredData.

0

5 Answers 5

2

This should work:

const dict = originalData.reduce((acc, obj) => {
  let groupId = obj['groupId'];
  delete obj.groupId;
  if (!acc[groupId]) {
    acc[groupId] = { // here is where we add the fields you wanted
        groupId,
        items: []
      };
  }
  acc[groupId].items.push(obj);
  return acc;
}, {});

// turn this into an array, just getting the values of the fields in the dictionary
const actualFormattedData = Object.values(dict);
Sign up to request clarification or add additional context in comments.

4 Comments

Awesome, thanks! I had to make a tweak to your function on line 4 (acc[key] -> acc[groupId]). Result is this which is what I was looking for: jsfiddle.net/u1fsngto
With this function, is it also possible to remove fields? I noticed that groupId is still present in the item objects, that's more of a nice-to-have. I can live with it being in both places I think, especially if would add a performance hit to try to remove fields while it's iterating.
I'm accepting this answer because I like that it only iterates through the array once. I also like that I don't have to explicitly set each field. Seems to be the most efficient and straightforward. As mentioned, it would be nice to be able to omit the groupId field inside of each item object, since it's redundant at that point, but whether or not that feature would be performant and/or easy to add, I still think this is the most elegant solution.
Yes, it's easy to remove the groupId field. I've added the necessary line (delete obj.groupId).
0

Here is your fiddle solution

https://jsfiddle.net/07n9ks86/

and the crucial code for it (n2):

const formattedData = originalData.reduce((acc, curr) => {
  console.log(acc)
  const index = acc.findIndex(x => x.group === curr.group);
  if (index > 0) {
    acc[index] = {
      ...acc[index],
      items: [...acc[index].items,
        {
          'color': curr.color,
          'shape': curr.shape
        }
      ]
    }
  } else {
    acc.push({
      group: curr.group,
      items: [{
        'color': curr.color,
        'shape': curr.shape
      }]
    })
  }
  return acc;
}, []);

1 Comment

Eugen, thanks for taking the time to provide this answer! I ended up going with Christian's solution because it only iterates through the data once (I'm assuming this findIndex is also looping through the array inside of the reduce function), but this is an interesting approach that I would not have thought of. Also I appreciate that you provided an updated fiddle in your answer!
0

A simple solution can be achieved with a single call to Array#reduce(), as detailed in the code snippet below.

Just a note that this solution emphasises simplicity over efficiency, and would tend to not be suitable for very large input arrays:

const originalData=[{groupId:0,color:"red",shape:"circle"},{groupId:1,color:"green",shape:"square"},{groupId:1,color:"orange",shape:"hexagon"},{groupId:1,color:"purple",shape:"triangle"},{groupId:2,color:"aqua",shape:"diamond"},{groupId:2,color:"blue",shape:"trapezoid"}];

/* Use reduce to iterate and transform originalData array to desired result */
const desiredData = originalData.reduce((result, item) => {
  
  /* The group item to add from this iteration */
  const groupItem = { color : item.color, shape : item.shape };
  
  /* Search for item that already exists with matching group id */
  const existingGroup = result.find(resultItem => resultItem.groupId === item.groupId);
  if(existingGroup) {
    /* Add item to group if found */
    existingGroup.items.push(groupItem);
  }
  else {
    /* Add group with item if not group found */
    result.push({ 
      groupId : item.groupId,
      items : [ groupItem ]
    });
  }
  
  return result;
  
}, []);

console.log(desiredData);

Hope that helps!

4 Comments

This should work but is not very efficient, since you will do a scan of the array in each iteration, O(n^2), when this can be done in O(n) by using an object (which has O(1) access by field name).
That's correct - I should preface this answer with a note that it emphasises simplicity over efficiency. If the data set were larger, an alternate solution would be a better fit.
Dacre, thanks for taking the time to answer my question. As Christian pointed out, performance might be a concern for the practical application of this function, as I don't know exactly how large the array might get. But this solution does indeed transform the data exactly as I had requested!
You're more than welcome - at the very least, this might help to show other ways reduce() can be used! All the best with your project :-)
0

Another simplest method to group by property name can be using lodash.

let groupedData = _.groupBy(rawData, dataObj => dataObj.propertyToGroupBy)

Where groupedData is the result you are looking for, rawData is the original data and propertyToGroupBy is the property of the object with which you want to group.

You can check this answer.

Comments

0

Here is an easy to understand solution:

const originalData = [
  {
    "groupId": 0,
    "color": "red",
    "shape": "circle"
  },
  {
    "groupId": 1,
    "color": "green",
    "shape": "square"
  },
  {
    "groupId": 1,
    "color": "orange",
    "shape": "hexagon"
  },
  {
    "groupId": 1,
    "color": "purple",
    "shape": "triangle"
  },
  {
    "groupId": 2,
    "color": "aqua",
    "shape": "diamond"
  },
  {
    "groupId": 2,
    "color": "blue",
    "shape": "trapezoid"
  }
];
const data = [];
const dataObjIndex = id=>{
  for(let i=0,l=data.length; i<l; i++){
    if(data[i].groupId === id){
      return i;
    }
  }
  return -1;
}
originalData.forEach(o=>{
  let i = dataObjIndex(o.groupId);
  if(i === -1){
    i = data.length; data.push({groupId:o.groupId, items:[]});
  }
  data[i].items.push({color:o.color, shape:o.shape});
});
console.log(data);

1 Comment

Thanks for your answer, StackSlave. As I mentioned above, I ended up going with Christian's answer because it was only iterating through the data once.

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.