'javascript | Object grouping

I have an object. It looks like below:

[
  {
    "name":"Display",
    "group":"Technical detals",
    "id":"60",
    "value":"4"
  },
  {
    "name":"Manufacturer",
    "group":"Manufacturer",
    "id":"58",
    "value":"Apple"
  },
  {
    "name":"OS",
    "group":"Technical detals",
    "id":"37",
    "value":"Apple iOS"
  }
]

I would like to group this data by group field and get this object:

var obj = {
    0 = [
    {
       'group'   = 'Technical detals',
       'name'    = 'Display',
       'id'      = '60',
       'value'   = '4'
    },
    {
       'group'   = 'Technical detals',
       'name'    = 'OS',
       'id'      = '37',
       'value'   = 'Apple iOS'
    }],
    1   = [
    {
       'group'   = 'Manufacturer',
       'name'    = 'Manufacturer',
       'id'      = '58',
       'value'   = 'Apple'
    }]
}

How can I group my first object?



Solution 1:[1]

Try with something like this:

function groupBy(collection, property) {
    var i = 0, val, index,
        values = [], result = [];
    for (; i < collection.length; i++) {
        val = collection[i][property];
        index = values.indexOf(val);
        if (index > -1)
            result[index].push(collection[i]);
        else {
            values.push(val);
            result.push([collection[i]]);
        }
    }
    return result;
}

var obj = groupBy(list, "group");

Keep in mind that Array.prototype.indexOf isn't defined in IE8 and older, but there are common polyfills for that.

Solution 2:[2]

Reduce is great for situations like this. Given list below is your input data:

const list = [{
    'name': 'Display',
    'group': 'Technical detals',
    'id': '60',
    'value': '4'
  },
  {
    'name': 'Manufacturer',
    'group': 'Manufacturer',
    'id': '58',
    'value': 'Apple'
  },
  {
    'name': 'OS',
    'group': 'Technical detals',
    'id': '37',
    'value': 'Apple iOS'
  }
];

const groups = list.reduce((groups, item) => {
  const group = (groups[item.group] || []);
  group.push(item);
  groups[item.group] = group;
  return groups;
}, {});

console.log(groups);

And if you wanted to be immutable, you could write the reduce like this:

const list = [{
    'name': 'Display',
    'group': 'Technical detals',
    'id': '60',
    'value': '4'
  },
  {
    'name': 'Manufacturer',
    'group': 'Manufacturer',
    'id': '58',
    'value': 'Apple'
  },
  {
    'name': 'OS',
    'group': 'Technical detals',
    'id': '37',
    'value': 'Apple iOS'
  }
];

const groups = list.reduce((groups, item) => ({
  ...groups,
  [item.group]: [...(groups[item.group] || []), item]
}), {});

console.log(groups);

Depending on whether your environment allows the spread syntax.

Solution 3:[3]

If you like working with ES6 Map, then this is for you:

function groupBy(arr, prop) {
    const map = new Map(Array.from(arr, obj => [obj[prop], []]));
    arr.forEach(obj => map.get(obj[prop]).push(obj));
    return Array.from(map.values());
}

const data = [{ name: "Display", group: "Technical detals", id: 60, value: 4 }, { name: "Manufacturer", group: "Manufacturer", id: 58, value: "Apple" }, { name: "OS", group: "Technical detals", id: 37, value: "Apple iOS" }];
	
console.log(groupBy(data, "group"));
.as-console-wrapper { max-height: 100% !important; top: 0; }

The Map instance is created from key/value pairs that are generated from the input array. The keys are the values of the property to group by, and the values are initialised as empty arrays.

Then those arrays are populated. Finally the values of the map (i.e. those populated arrays) are returned.

Solution 4:[4]

If you're using underscore.js in your application then you can simply do the following:

var groups = _.groupBy(data, 'group'); // data is your initial collection

Or if you prefer not to use any library then you can do it yourself:

var groups = { };
data.forEach(function(item){
   var list = groups[item.group];

   if(list){
       list.push(item);
   } else{
      groups[item.group] = [item];
   }
});

You can see both examples in action http://jsfiddle.net/nkVu6/3/

Solution 5:[5]

Use reduce and filter.

lets say your initial array is assigned to data

data.reduce((acc, d) => {
    if (Object.keys(acc).includes(d.group)) return acc;

    acc[d.group] = data.filter(g => g.group === d.group); 
    return acc;
}, {})

this will give you something like

{
    "Technical detals" = [
    {
       'group'   = 'Technical detals',
       'name'    = 'Display',
       'id'      = '60',
       'value'   = '4'
    },
    {
       'group'   = 'Technical detals',
       'name'    = 'OS',
       'id'      = '37',
       'value'   = 'Apple iOS'
    }],
    "Manufacturer"   = [
    {
       'group'   = 'Manufacturer',
       'name'    = 'Manufacturer',
       'id'      = '58',
       'value'   = 'Apple'
    }]
}

Solution 6:[6]

You could use a hash table for the groups and Array#forEach for iterating the array.

Then check if the hash exist and if not assign an empty array to it and push it to the result set.

Later push the actual element to the array of the hash.

function groupBy(array, group) {
    var hash = Object.create(null),
        result = [];

    array.forEach(function (a) {
        if (!hash[a[group]]) {
            hash[a[group]] = [];
            result.push(hash[a[group]]);
        }
        hash[a[group]].push(a);
    });
    return result;
}

var data = [{ name: "Display", group: "Technical detals", id: 60, value: 4 }, { name: "Manufacturer", group: "Manufacturer", id: 58, value: "Apple" }, { name: "OS", group: "Technical detals", id: 37, value: "Apple iOS" }];
    
console.log(groupBy(data, "group"));
.as-console-wrapper { max-height: 100% !important; top: 0; }

UPDATE 2022

You could take the possible upcoming Array#groupBy or a polyfill and take only the values of the object.

This methods need a callback for a grouping value.

({ group }) => group    // take property group and return it

Array.prototype.groupBy ??= function(callbackfn, thisArg) {
    const
        O = Object(this),
        len = O.length >>> 0;

    if (typeof callbackfn !== 'function') {
        throw new TypeError(callbackfn + ' is not a function');
    }

    let k = 0;
    const groups = {};

    while (k < len) {
        const
            Pk = Number(k).toString(),
            kValue = O[Pk],
            propertyKey = callbackfn.call(thisArg, kValue, Number(k), O);

        (groups[propertyKey] ??= []).push(kValue);
        ++k;
    }
    return groups;
}

const
    data = [{ name: "Display", group: "Technical details", id: 60, value: 4 }, { name: "Manufacturer", group: "Manufacturer", id: 58, value: "Apple" }, { name: "OS", group: "Technical details", id: 37, value: "Apple iOS" }],
    result = Object.values(data.groupBy(({ group }) => group));

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

Solution 7:[7]

If you are using lodash, you can use groupBy.

It supports both array and object.

Example:

_.groupBy([6.1, 4.2, 6.3], Math.floor);
// => { '4': [4.2], '6': [6.1, 6.3] }

// The `_.property` iteratee shorthand.
_.groupBy(['one', 'two', 'three'], 'length');
// => { '3': ['one', 'two'], '5': ['three'] }

Solution 8:[8]

A bit different way, so we have a plain list of objects, and want to group it per property, but includes all related

const data = [{'group':'1', 'name':'name1'},
{'group':'2', 'name':'name2'},
{'group':'2', 'name':'name3'},
,{'group':'1', 'name':'name4'}]; 

const list = data.map( i => i.group);
const uniqueList = Array.from(new Set(list));
const groups= uniqueList.map( c => { 
            return  { group:c, names:[]};
        } ); 

data.forEach( d => { 
            groups.find( g => g.group === d.group).names.push(d.name);
});

so result would be like:

[ {'group':'1', 'names':['name1', 'name4']},
{'group':'2', 'names':['name2', 'name3']}

Same but with TypeScript and reduce:

export function groupBy2 <T>( key: string, arr: T[]): { group: string, values: T[] }[] {
     return arr.reduce((storage, item) => {
         const a = storage.find(g => g.group === item[key]);
         if (a) { a.values.push(item); }
         else { storage.push({group: item[key], values: [item]}); }
         return storage;
     }, [] as {group: string, values: T[] }[]);
 }

Solution 9:[9]

Based on Anthony Awuley answer I prepared generic solution for TypeScript!

const groupBy = <T, K extends keyof T>(value: T[], key: K) =>
  value.reduce((acc, curr) => {
    if (acc.get(curr[key])) return acc;
    acc.set(curr[key], value.filter(elem => elem[key] === curr[key]));
    return acc;
  }, new Map<T[K], T[]>());

Solution 10:[10]

I tried to use the answer marked as accepted, but noticed that there were elements missing in some groups, depending on the type of property being evaluated. This is a solution derived from that answer:

function groupBy(collection, property) {
  var i = 0, values = [], result = [];
  for (i; i < collection.length; i++) {
    if(values.indexOf(collection[i][property]) === -1) {
      values.push(collection[i][property]);
      result.push(collection.filter(function(v) { return v[property] === collection[i][property] }));
    }
  }
  return result;
}
var obj = groupBy(list, "group");

Solution 11:[11]

This works using a key-getter so you can dynamically group an array's objects.

const groupBy = (arr, keyGetter) => {
  const out = {};
  for (let item of arr) {
    const key = keyGetter(item);
    out[key] ??= [];
    out[key].push(item);
  }
  return out;
};

const tasks = [
  {task: 'Do the laundry', dueTime: "4/27 13:00"},
  {task: 'Do the dishes', dueTime: "4/27 15:30"},
  {task: 'Do homework', dueTime: "4/28 13:30"},
  {task: 'Clean the garage', dueTime: "4/29 14:00"}
];

// Group by the day a task is due
const grouped = groupBy(tasks, (item) => item.dueTime.slice(0, -6));
console.log(grouped);

Solution 12:[12]

Job done by this function :

export function ObjGroupBy(list, key) {
  return list.reduce(
    (groups, item) => ({
      ...groups,
      [item[key]]: [...(groups[item[key]] || []), item],
    }),
    {}
  )
}