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.

Aaron Sarnat
  • 1,207
  • 8
  • 16

5 Answers5

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);
Christian Fritz
  • 20,641
  • 3
  • 42
  • 71
  • 1
    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: https://jsfiddle.net/u1fsngto/ – Aaron Sarnat Jan 29 '20 at 00:51
  • 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. – Aaron Sarnat Jan 29 '20 at 00:55
  • 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. – Aaron Sarnat Jan 29 '20 at 02:21
  • 1
    Yes, it's easy to remove the `groupId` field. I've added the necessary line (`delete obj.groupId`). – Christian Fritz Jan 29 '20 at 04:22
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!

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
  • 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). – Christian Fritz Jan 29 '20 at 00:36
  • 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 Denny Jan 29 '20 at 00:38
  • 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! – Aaron Sarnat Jan 29 '20 at 02:25
  • 1
    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 :-) – Dacre Denny Jan 29 '20 at 02:28
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;
}, []);
EugenSunic
  • 13,162
  • 13
  • 64
  • 86
  • 1
    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! – Aaron Sarnat Jan 29 '20 at 02:34
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);
StackSlave
  • 10,613
  • 2
  • 18
  • 35
  • 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. – Aaron Sarnat Jan 29 '20 at 02:37
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.

shivamragnar
  • 383
  • 2
  • 10