Group array items using object

Solution 1:

Start by creating a mapping of group names to values. Then transform into your desired format.

var myArray = [
    {group: "one", color: "red"},
    {group: "two", color: "blue"},
    {group: "one", color: "green"},
    {group: "one", color: "black"}
];

var group_to_values = myArray.reduce(function (obj, item) {
    obj[item.group] = obj[item.group] || [];
    obj[item.group].push(item.color);
    return obj;
}, {});

var groups = Object.keys(group_to_values).map(function (key) {
    return {group: key, color: group_to_values[key]};
});

var pre = document.createElement("pre");
pre.innerHTML = "groups:\n\n" + JSON.stringify(groups, null, 4);
document.body.appendChild(pre);

Using Array instance methods such as reduce and map gives you powerful higher-level constructs that can save you a lot of the pain of looping manually.

Solution 2:

First, in JavaScript it's generally not a good idea to iterate over arrays using for ... in. See Why is using "for...in" with array iteration a bad idea? for details.

So you might try something like this:

var groups = {};
for (var i = 0; i < myArray.length; i++) {
  var groupName = myArray[i].group;
  if (!groups[groupName]) {
    groups[groupName] = [];
  }
  groups[groupName].push(myArray[i].color);
}
myArray = [];
for (var groupName in groups) {
  myArray.push({group: groupName, color: groups[groupName]});
}

Using the intermediary groups object here helps speed things up because it allows you to avoid nesting loops to search through the arrays. Also, because groups is an object (rather than an array) iterating over it using for ... in is appropriate.

Addendum

FWIW, if you want to avoid duplicate color entries in the resulting arrays you could add an if statement above the line groups[groupName].push(myArray[i].color); to guard against duplicates. Using jQuery it would look like this;

if (!$.inArray(myArray[i].color, groups[groupName])) {
  groups[groupName].push(myArray[i].color);
}

Without jQuery you may want to add a function that does the same thing as jQuery's inArray:

Array.prototype.contains = function(value) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] === value)
      return true;
  }
  return false;
}

and then use it like this:

if (!groups[groupName].contains(myArray[i].color)) {
  groups[groupName].push(myArray[i].color);
}

Note that in either case you are going to slow things down a bit due to all the extra iteration, so if you don't need to avoid duplicate color entries in the result arrays I would recommend avoiding this extra code. There

Solution 3:

Use lodash's groupby method

Creates an object composed of keys generated from the results of running each element of collection thru iteratee. The order of grouped values is determined by the order they occur in collection. The corresponding value of each key is an array of elements responsible for generating the key. The iteratee is invoked with one argument: (value).

So with lodash you can get what you want in a single line. See below

let myArray = [
  {group: "one", color: "red"},
  {group: "two", color: "blue"},
  {group: "one", color: "green"},
  {group: "one", color: "black"},
]
let grouppedArray=_.groupBy(myArray,'group')
console.log(grouppedArray)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>

Solution 4:

Using ES6, this can be done quite nicely using .reduce() with a Map as the accumulator, and then using Array.from() with its mapping function to map each grouped map-entry to an object:

const arr = [{"group":"one","color":"red"},{"group":"two","color":"blue"},{"group":"one","color":"green"},{"group":"one","color":"black"}];

const res = Array.from(arr.reduce((m, {group, color}) => 
    m.set(group, [...(m.get(group) || []), color]), new Map
  ), ([group, color]) => ({group, color})
);

console.log(res);

The above as a reusable function:

const arr = [{"group":"one","color":"red"},{"group":"two","color":"blue"},{"group":"one","color":"green"},{"group":"one","color":"black"}];

const groupAndMerge = (arr, groupBy, mergeInto) => {
  return Array.from(arr.reduce((m, obj) => 
      m.set(obj[groupBy], [...(m.get(obj[groupBy]) || []), obj[mergeInto]]), new Map
    ), ([grouped, merged]) => ({[groupBy]: grouped, [mergeInto]: merged})
  );
};

console.log(groupAndMerge(arr, "group", "color"));

If you have additional properties in your objects other than just group and color, you can take a more general approach by setting a grouped object as the map's values like so:

const arr = [{"group":"one","color":"red"},{"group":"two","color":"blue"},{"group":"one","color":"green"},{"group":"one","color":"black"}];

const groupAndMerge = (arr, groupBy, mergeInto) => 
  Array.from(arr.reduce((m, o) => {
    const curr = m.get(o[groupBy]);
    return m.set(o[groupBy], {...o, [mergeInto]: [...(curr && curr[mergeInto] || []), o[mergeInto]]});
  }, new Map).values());

console.log(groupAndMerge(arr, 'group', 'color'));

If you can support optional chaining and the nullish coalescing operator (??), you can simplify the above method to the following:

const arr = [{"group":"one","color":"red"},{"group":"two","color":"blue"},{"group":"one","color":"green"},{"group":"one","color":"black"}];
const groupAndMerge = (arr, groupBy, mergeWith) =>
  Array.from(arr.reduce((m, o) => m.set(o[groupBy], {...o, [mergeWith]: [...(m.get(o[groupBy])?.[mergeWith] ?? []), o[mergeWith]]}), new Map).values());

console.log(groupAndMerge(arr, 'group', 'color'));