2

I am trying to determine the best data-type to sort an array of objects, into groups defined by properties. I have an array of objects, like so:

var people = [
    {
        name: 'Pete',
        gender: 'Male',
        age: 22

    },
    {
        name: 'Samantha',
        gender: 'Female',
        age: 20

    },
    {
        name: 'Frank',
        gender: 'Male',
        age: 22

    },
    {
        name: 'Gary',
        gender: 'Male',
        age: 21

    },
    {
        name: 'Maria',
        gender: 'Female',
        age: 20

    },
    {
        name: 'Hannah',
        gender: 'Female',
        age: 21

    },
    {
        name: 'Pete',
        gender: 'Male',
        age: 20

    }
];

I need to group these objects into an arbitrary-defined group. E.g.:

  1. Group 1: gender
  2. Group 2: age

(This can be defined by the server and can change to contain a third group if we wish.)

Which then gives me (visually):

Male:
   21:
     Gary
   22:
     Pete
     Frank

Female
   20:
     Samantha
     Maria
   21:
     Hannah

I think the appropriate data type would be an object of objects. I.e.:

{
    Male: {
        21: {
            [
                {
                    name: 'Gary',
                    gender: 'Male',
                    age: 21

                }
            ]
        },
        22: {
            [
                {
                    name: 'Pete',
                    gender: 'Male',
                    age: 22

                },
                {
                    name: 'Frank',
                    gender: 'Male',
                    age: 22

                }
            ]
        }
    },
    Female: {
        20: {
            [
                {
                    name: 'Samantha',
                    gender: 'Female',
                    age: 20

                },
                {
                    name: 'Maria',
                    gender: 'Female',
                    age: 20

                }
            ]
        },
        21: {
            [
                {
                    name: 'Hannah',
                    gender: 'Female',
                    age: 21

                }
            ]
        }
    }
}

But I cannot work out, for the life of me, an appropriate algorithm to sort these objects into a data-type which represents the above.

There is a useful utility in underscore.js called _.groupBy(arr, callback) which I can use as follows:

_.groupBy(people, function (person) {
    var props = ['gender', 'age'], // server-defined
        prop = [];

    for (var i = 0, length = props.length; i < length; i++) {
        prop.push(person[props[i]]);
    }

    return prop.join('|');
});

This gives me a 1-depth object which I can use a for...in loop to iterate over the keys, and loop through to build the object above, but it's that bit of the code I'm stuck on.

The returned object would be:

{
    "Male|22": [
        {
            "name": "Pete",
            "gender": "Male",
            "age": 22
        },
        {
            "name": "Frank",
            "gender": "Male",
            "age": 22
        }
    ],
    "Female|20": [
        {
            "name": "Samantha",
            "gender": "Female",
            "age": 20
        },
        {
            "name": "Maria",
            "gender": "Female",
            "age": 20
        }
    ],
    "Male|21": [
        {
            "name": "Gary",
            "gender": "Male",
            "age": 21
        }
    ],
    "Female|21": [
        {
            "name": "Hannah",
            "gender": "Female",
            "age": 21
        }
    ],
    "Male|20": [
        {
            "name": "Pete",
            "gender": "Male",
            "age": 20
        }
    ]
}

I'm thinking of then looping through each key in the object, splitting at the pipe (|) and using recursion to construct a new object of objects containing the groups/array of data.

That's such a horrendous way of achieving this but I have no idea how to do it otherwise.

Is there a better way I'm missing?

keldar
  • 6,152
  • 10
  • 52
  • 82

5 Answers5

13

Maybe this helps you. It utilises an array with the properties of the object and the result is grouped by the content of the properties.

The forEach loop iterates over the data. The reduce loop is for generating grouped properties for every given group and if it is the last one it returns an array, if not already there.

The last step is to push the value of one of people to the array.

var people = [{ name: 'Pete', gender: 'Male', age: 22 }, { name: 'Samantha', gender: 'Female', age: 20 }, { name: 'Frank', gender: 'Male', age: 22 }, { name: 'Gary', gender: 'Male', age: 21 }, { name: 'Maria', gender: 'Female', age: 20 }, { name: 'Hannah', gender: 'Female', age: 21 }, { name: 'Pete', gender: 'Male', age: 20 }],
    groups = ['gender', 'age'],
    grouped = {};

people.forEach(function (a) {
    groups.reduce(function (o, g, i) {                            // take existing object,
        o[a[g]] = o[a[g]] || (i + 1 === groups.length ? [] : {}); // or generate new obj, or
        return o[a[g]];                                           // at last, then an array
    }, grouped).push(a);
});

document.write('<pre>' + JSON.stringify(grouped, 0, 4) + '</pre>');
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • This works! And grouping allows for an arbitrary length. :) Can you describe it a little more in detail? – keldar Feb 19 '16 at 14:29
  • I had something similar - but it was hideously verbose and wanted something far more terse :) - this is awesome! – keldar Feb 19 '16 at 14:37
4

you can use lodash

groupBy(people, (item) => item.age + item.gender);
Josh Rice
  • 51
  • 3
2

I realy don't understand why people always use frameworks for that kind of things. Vanilla is faster and this is not too hard to code...

var people = [    { name: 'Pete', gender: 'Male', age: 22 },     { name: 'Samantha', gender: 'Female', age: 20 },     { name: 'Frank', gender: 'Male', age: 22 },     { name: 'Gary', gender: 'Male', age: 21 },     { name: 'Maria', gender: 'Female', age: 20 },     { name: 'Hannah', gender: 'Female', age: 21 }, { name: 'Pete', gender: 'Male', age: 20 }];


var grouped = {}; // final object 
for (var i=0,len=people.length,p;i<len;i++) { // faster than .forEach
  p = people[i];

  if (grouped[p.gender] === undefined) // twice faster then hasOwnProperty
    grouped[p.gender] = {};

  if (grouped[p.gender][p.age] === undefined)
    grouped[p.gender][p.age] = [];

  grouped[p.gender][p.age].push(p); // your groupby is HERE xD
}
document.getElementById('grouped').innerHTML = JSON.stringify(grouped, null, 2)
<pre id="grouped"></pre>
Patrick Ferreira
  • 1,983
  • 1
  • 15
  • 31
1

I'm going to disagree with you about your expected output. I think you should just have an object with Male and Female array properties and use this code to populate the arrays:

var obj = people.reduce(function (p, c, i) {
  p[c.gender].push(c); 
  return p;
}, { Male: [], Female: [] });

If you want to then filter those arrays you can write functions like this:

function getMaleByAge(obj, age) {
  return obj.Male.filter(function (el) {
    return el.age === age;
  })
}

getMaleByAge(obj, 21); // { name: "Gary", gender: "Male", age: 21 }

DEMO

Andy
  • 61,948
  • 13
  • 68
  • 95
  • Thanks @andy - only problem is, the object *may* contain more properties (I used this example to simplify the scenario) and the groups could be an arbitrary level. So each `person` object can contain, say as an example, 20 properties and we might want to organise those into 5 groups. Make sense? – keldar Feb 19 '16 at 13:48
  • 1
    Then just filter on the original data as you see fit. Just women? `people.filter(function (el) { return el.gender === 'Female' ; });` Instead of rearranging the whole dataset, just pick out the data groups you need to work with. – Andy Feb 19 '16 at 13:53
  • 1
    Even women aged 21: `people.filter(function (el) { return el.gender === 'Female' && el.age === 21; });`. Simple. I think you're making a lot more work for yourself doing it the way you planned. Hope it works out regardless :) – Andy Feb 19 '16 at 14:11
  • Thanks Andy - the complication comes in from the list of arbitrary fields (I also plan to build a UI too). But your solution has helped nonetheless! – keldar Feb 19 '16 at 14:25
1

Using _.groupBy & _.map

var output = _.map(_.groupBy(people, 'gender'),function(obj, key){
  var temp = {};
  temp[key] = _.groupBy(obj, 'age')
  return temp;
});

console.log(JSON.stringify(output, null,' '))

Will give following output

 [{
  "Male": {
   "20": [
    {
     "name": "Pete",
     "gender": "Male",
     "age": 20
    }
   ],
   "21": [
    {
     "name": "Gary",
     "gender": "Male",
     "age": 21
    }
   ],
   "22": [
    {
     "name": "Pete",
     "gender": "Male",
     "age": 22
    },
    {
     "name": "Frank",
     "gender": "Male",
     "age": 22
    }
   ]
  }
 },
 {
  "Female": {
   "20": [
    {
     "name": "Samantha",
     "gender": "Female",
     "age": 20
    },
    {
     "name": "Maria",
     "gender": "Female",
     "age": 20
    }
   ],
   "21": [
    {
     "name": "Hannah",
     "gender": "Female",
     "age": 21
    }
   ]
  }
 }
]
Serg
  • 2,346
  • 3
  • 29
  • 38
Jagdish Idhate
  • 7,513
  • 9
  • 35
  • 51