[javascript] Group array items using object

My array is something like this:

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

I want to convert this into:

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

So, basically, group by group.

I'm trying:

for (i in myArray){
  var group = myArray[i].group;
  //myArray.push(group, {???})
}

I just don't know how to handle the grouping of similar group values.

This question is related to javascript

The answer is


I like to use the Map constructor callback for creating the groups (map keys). The second step is to populate the values of that map, and finally to extract the map's data in the desired output format:

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

let map = new Map(myArray.map(({group}) => [group, { group, color: [] }]));
for (let {group, color} of myArray) map.get(group).color.push(color);
let result = [...map.values()];

console.log(result);

 
_x000D_
_x000D_
_x000D_


This version takes advantage that object keys are unique. We process the original array and collect the colors by group in a new object. Then create new objects from that group -> color array map.

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

    //new object with keys as group and
    //color array as value
    var newArray = {};

    //iterate through each element of array
    myArray.forEach(function(val) {
      var curr = newArray[val.group]

      //if array key doesnt exist, init with empty array
      if (!curr) {
        newArray[val.group] = [];
      }

      //append color to this key
      newArray[val.group].push(val.color);
    });

    //remove elements from previous array
    myArray.length = 0;

    //replace elements with new objects made of
    //key value pairs from our created object
    for (var key in newArray) {
      myArray.push({
        'group': key,
        'color': newArray[key]
      });
    }

Please note that this does not take into account duplicate colors of the same group, so it is possible to have multiple of the same color in the array for a single group.


_x000D_
_x000D_
myArray = [_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "green"},_x000D_
  {group: "one", color: "black"}_x000D_
];_x000D_
_x000D_
_x000D_
let group = myArray.map((item)=>  item.group ).filter((item, i, ar) => ar.indexOf(item) === i).sort((a, b)=> a - b).map(item=>{_x000D_
    let new_list = myArray.filter(itm => itm.group == item).map(itm=>itm.color);_x000D_
    return {group:item,color:new_list}_x000D_
});_x000D_
console.log(group);
_x000D_
_x000D_
_x000D_


this repo offers solutions in lodash and alternatives in native Js, you can find how to implement groupby. https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_groupby


var array = [{
      id: "123",
      name: "aaaaaaaa"
    }, {
      id: "123",
      name: "aaaaaaaa"
    }, {
      id: '456',
      name: 'bbbbbbbbbb'
    }, {
      id: '789',
      name: 'ccccccccc'
    }, {
      id: '789',
      name: 'ccccccccc'
    }, {
      id: '098',
      name: 'dddddddddddd'
    }];
//if you want to group this array
group(array, key) {
  console.log(array);
  let finalArray = [];
  array.forEach(function(element) {
    var newArray = [];
    array.forEach(function(element1) {
      if (element[key] == element1[key]) {
          newArray.push(element)
      }
    });
    if (!(finalArray.some(arrVal => newArray[0][key] == arrVal[0][key]))) {
        finalArray.push(newArray);
    }
  });
  return finalArray
}
//and call this function
groupArray(arr, key) {
  console.log(this.group(arr, key))
}

Using Array's reduce and findIndex methods, this can be achieved.

_x000D_
_x000D_
var myArray = [{_x000D_
  group: "one",_x000D_
  color: "red"_x000D_
}, {_x000D_
  group: "two",_x000D_
  color: "blue"_x000D_
}, {_x000D_
  group: "one",_x000D_
  color: "green"_x000D_
}, {_x000D_
  group: "one",_x000D_
  color: "black"_x000D_
}];_x000D_
_x000D_
var transformedArray = myArray.reduce((acc, arr) => {_x000D_
  var index = acc.findIndex(function(element) {_x000D_
    return element.group === arr.group;_x000D_
  });_x000D_
  if (index === -1) {_x000D_
    return acc.push({_x000D_
      group: arr.group,_x000D_
      color: [arr.color]_x000D_
    });_x000D_
  }_x000D_
  _x000D_
  acc[index].color.push(arr.color);_x000D_
  return acc;_x000D_
}, []);_x000D_
_x000D_
console.log(transformedArray);
_x000D_
_x000D_
_x000D_

By using reduce function, array is iterator and the new values are stored in acc (accumulating) parameter. To check if the object with given group already exists we can use findIndex function.

If findIndex() return -1, the value does not exist, so add the array in the acc parameter.

If findIndex() return index, then update the index with the arr values.


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:

_x000D_
_x000D_
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);
_x000D_
_x000D_
_x000D_

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:

_x000D_
_x000D_
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'));
_x000D_
_x000D_
_x000D_

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

_x000D_
_x000D_
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'));
_x000D_
_x000D_
_x000D_


Try (h={})

myArray.forEach(x=> h[x.group]= (h[x.group]||[]).concat(x.color) );
myArray = Object.keys(h).map(k=> ({group:k, color:h[k]}))

_x000D_
_x000D_
let myArray = [_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "green"},_x000D_
  {group: "one", color: "black"},_x000D_
];_x000D_
_x000D_
let h={};_x000D_
_x000D_
myArray.forEach(x=> h[x.group]= (h[x.group]||[]).concat(x.color) );_x000D_
myArray = Object.keys(h).map(k=> ({group:k, color:h[k]}))_x000D_
_x000D_
console.log(myArray);
_x000D_
_x000D_
_x000D_


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

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


One option is:

var res = myArray.reduce(function(groups, currentValue) {
    if ( groups.indexOf(currentValue.group) === -1 ) {
      groups.push(currentValue.group);
    }
    return groups;
}, []).map(function(group) {
    return {
        group: group,
        color: myArray.filter(function(_el) {
          return _el.group === group;
        }).map(function(_el) { return _el.color; })
    }
});

http://jsfiddle.net/dvgwodxq/


Beside the given approaches with a two pass approach, you could take a single loop approach by pushing the group if a new group is found.

_x000D_
_x000D_
var array = [{ group: "one", color: "red" }, { group: "two", color: "blue" }, { group: "one", color: "green" }, { group: "one", color: "black" }],_x000D_
    groups = Object.create(null),_x000D_
    grouped = [];_x000D_
_x000D_
array.forEach(function (o) {_x000D_
    if (!groups[o.group]) {_x000D_
        groups[o.group] = [];_x000D_
        grouped.push({ group: o.group, color: groups[o.group] });_x000D_
    }_x000D_
    groups[o.group].push(o.color);_x000D_
});_x000D_
_x000D_
console.log(grouped);
_x000D_
.as-console-wrapper { max-height: 100% !important; top: 0; }
_x000D_
_x000D_
_x000D_


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

_x000D_
_x000D_
var myArray = [_x000D_
    {group: "one", color: "red"},_x000D_
    {group: "two", color: "blue"},_x000D_
    {group: "one", color: "green"},_x000D_
    {group: "one", color: "black"}_x000D_
];_x000D_
_x000D_
var group_to_values = myArray.reduce(function (obj, item) {_x000D_
    obj[item.group] = obj[item.group] || [];_x000D_
    obj[item.group].push(item.color);_x000D_
    return obj;_x000D_
}, {});_x000D_
_x000D_
var groups = Object.keys(group_to_values).map(function (key) {_x000D_
    return {group: key, color: group_to_values[key]};_x000D_
});_x000D_
_x000D_
var pre = document.createElement("pre");_x000D_
pre.innerHTML = "groups:\n\n" + JSON.stringify(groups, null, 4);_x000D_
document.body.appendChild(pre);
_x000D_
_x000D_
_x000D_

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.


You can do something like this:

function convert(items) {
    var result = [];

    items.forEach(function (element) {
        var existingElement = result.filter(function (item) {
            return item.group === element.group;
        })[0];

        if (existingElement) {
            existingElement.color.push(element.color);
        } else {
            element.color = [element.color];
            result.push(element);
        }

    });

    return result;
}

This gives you unique colors, if you do not want duplicate values for color

_x000D_
_x000D_
var arr = [_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "green"},_x000D_
  {group: "one", color: "black"}_x000D_
]_x000D_
_x000D_
var arra = [...new Set(arr.map(x => x.group))]_x000D_
_x000D_
let reformattedArray = arra.map(obj => {_x000D_
   let rObj = {}_x000D_
   rObj['color'] = [...new Set(arr.map(x => x.group == obj ? x.color:false ))]_x000D_
       .filter(x => x != false)_x000D_
   rObj['group'] = obj_x000D_
   return rObj_x000D_
})_x000D_
console.log(reformattedArray)
_x000D_
_x000D_
_x000D_


My approach with a reducer:

_x000D_
_x000D_
myArray = [_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "green"},_x000D_
  {group: "one", color: "black"}_x000D_
]_x000D_
_x000D_
console.log(myArray.reduce( (acc, curr) => {_x000D_
  const itemExists = acc.find(item => curr.group === item.group)_x000D_
  if(itemExists){_x000D_
    itemExists.color = [...itemExists.color, curr.color]_x000D_
  }else{_x000D_
    acc.push({group: curr.group, color: [curr.color]})_x000D_
  }_x000D_
  return acc;_x000D_
}, []))
_x000D_
_x000D_
_x000D_


Another option is using reduce() and new Map() to group the array. Use Spread syntax to convert set object into an array.

_x000D_
_x000D_
var myArray = [{"group":"one","color":"red"},{"group":"two","color":"blue"},{"group":"one","color":"green"},{"group":"one","color":"black"}]_x000D_
_x000D_
var result = [...myArray.reduce((c, {group,color}) => {_x000D_
  if (!c.has(group)) c.set(group, {group,color: []});_x000D_
  c.get(group).color.push(color);_x000D_
  return c;_x000D_
}, new Map()).values()];_x000D_
_x000D_
console.log(result);
_x000D_
_x000D_
_x000D_


You can extend array functionality with the next:

Array.prototype.groupBy = function(prop) {
  var result = this.reduce(function (groups, item) {
      const val = item[prop];
      groups[val] = groups[val] || [];
      groups[val].push(item);
      return groups;
  }, {});
  return Object.keys(result).map(function(key) {
      return result[key];
  });
};

Usage example:

_x000D_
_x000D_
/* re-usable function */_x000D_
Array.prototype.groupBy = function(prop) {_x000D_
  var result = this.reduce(function (groups, item) {_x000D_
      const val = item[prop];_x000D_
      groups[val] = groups[val] || [];_x000D_
      groups[val].push(item);_x000D_
      return groups;_x000D_
  }, {});_x000D_
  return Object.keys(result).map(function(key) {_x000D_
      return result[key];_x000D_
  });_x000D_
};_x000D_
_x000D_
var myArray = [_x000D_
  {group: "one", color: "red"},_x000D_
  {group: "two", color: "blue"},_x000D_
  {group: "one", color: "green"},_x000D_
  {group: "one", color: "black"}_x000D_
]_x000D_
_x000D_
console.log(myArray.groupBy('group'));
_x000D_
_x000D_
_x000D_

Credits: @Wahinya Brian