rickysullivan
rickysullivan

Reputation: 443

Underscore.js groupBy multiple values

Using Underscore.js, I'm trying to group a list of items multiple times, ie

Group by SIZE then for each SIZE, group by CATEGORY...

http://jsfiddle.net/rickysullivan/WTtXP/1/

Ideally, I'd like to have a function or extend _.groupBy() so that you can throw an array at it with the paramaters to group by.

var multiGroup = ['size', 'category'];

Probably could just make a mixin...

_.mixin({
    groupByMulti: function(obj, val, arr) {
        var result = {};
        var iterator = typeof val == 'function' ? val : function(obj) {
                return obj[val];
            };
        _.each(arr, function(arrvalue, arrIndex) {
            _.each(obj, function(value, objIndex) {
                var key = iterator(value, objIndex);
                var arrresults = obj[objIndex][arrvalue];
                if (_.has(value, arrvalue))
                    (result[arrIndex] || (result[arrIndex] = [])).push(value);

My head hurts, but I think some more pushing needs to go here...

            });
        })
        return result;
    }
});

properties = _.groupByMulti(properties, function(item) {

    var testVal = item["size"];

    if (parseFloat(testVal)) {
        testVal = parseFloat(item["size"])
    }

    return testVal

}, multiGroup);

Upvotes: 25

Views: 32796

Answers (9)

2Toad
2Toad

Reputation: 15038

Grouping by a composite key tends to work better for me in most situations:

const groups = _.groupByComposite(myList, ['size', 'category']);

Demo using OP's fiddle

Mixin

_.mixin({
  /*
   * @groupByComposite
   *
   * Groups an array of objects by multiple properties. Uses _.groupBy under the covers,
   * to group by a composite key, generated from the list of provided keys.
   *
   * @param {Object[]} collection - the array of objects.
   * @param {string[]} keys - one or more property names to group by.
   * @param {string} [delimiter=-] - a delimiter used in the creation of the composite key.
   *
   * @returns {Object} - the composed aggregate object.
   */
  groupByComposite: (collection, keys, delimiter = '-') =>
    _.groupBy(collection, (item) => {
      const compositeKey = [];
      _.each(keys, key => compositeKey.push(item[key]));
      return compositeKey.join(delimiter);
    }),
});

Upvotes: 0

Muhammad Kashif Nazar
Muhammad Kashif Nazar

Reputation: 23865

Here is an easy to understand function.

function mixin(list, properties){

    function grouper(i, list){

        if(i < properties.length){
            var group = _.groupBy(list, function(item){
                var value = item[properties[i]];
                delete item[properties[i]];
                return value;
            });

            _.keys(group).forEach(function(key){
                group[key] = grouper(i+1, group[key]);
            });
            return group;
        }else{
            return list;
        }
    }

    return grouper(0, list);

}

Upvotes: 0

Bergi
Bergi

Reputation: 664538

A simple recursive implementation:

_.mixin({
  /*
   * @mixin
   *
   * Splits a collection into sets, grouped by the result of running each value
   * through iteratee. If iteratee is a string instead of a function, groups by
   * the property named by iteratee on each of the values.
   *
   * @param {array|object} list - The collection to iterate over.
   * @param {(string|function)[]} values - The iteratees to transform keys.
   * @param {object=} context - The values are bound to the context object.
   * 
   * @returns {Object} - Returns the composed aggregate object.
   */
  groupByMulti: function(list, values, context) {
    if (!values.length) {
      return list;
    }
    var byFirst = _.groupBy(list, values[0], context),
        rest    = values.slice(1);
    for (var prop in byFirst) {
      byFirst[prop] = _.groupByMulti(byFirst[prop], rest, context);
    }
    return byFirst;
  }
});

Demo in your jsfiddle

Upvotes: 41

Marcel Santos
Marcel Santos

Reputation: 1

An example with lodash and mixin

_.mixin({
'groupByMulti': function (collection, keys) {
if (!keys.length) {
 return collection;
 } else {
  return _.mapValues(_.groupBy(collection,_.first(keys)),function(values) {
    return _.groupByMulti(values, _.rest(keys));
  });
}
}
});    

Upvotes: 0

Vedant Misra
Vedant Misra

Reputation: 85

The improvements by joyrexus on bergi's method don't take advantage of the underscore/lodash mixin system. Here it is as a mixin:

_.mixin({
  nest: function (collection, keys) {
    if (!keys.length) {
      return collection;
    } else {
      return _(collection).groupBy(keys[0]).mapValues(function(values) {
        return _.nest(values, keys.slice(1));
      }).value();
    }
  }
});

Upvotes: 0

joyrexus
joyrexus

Reputation: 400

I think @Bergi's answer can be streamlined a bit by utilizing Lo-Dash's mapValues (for mapping functions over object values). It allows us to group the entries in an array by multiple keys in a nested fashion:

_ = require('lodash');

var _.nest = function (collection, keys) {
  if (!keys.length) {
    return collection;
  }
  else {
    return _(collection).groupBy(keys[0]).mapValues(function(values) { 
      return nest(values, keys.slice(1));
    }).value();
  }
};

I renamed the method to nest because it ends up serving much the same role served by D3's nest operator. See this gist for details and this fiddle for demonstrated usage with your example.

Upvotes: 17

Tyler
Tyler

Reputation: 81

This is a great use case for the reduce phase of map-reduce. It's not going to be as visually elegant as the multi-group function (you can't just pass in an array of keys to group on), but overall this pattern gives you more flexibility to transform your data. EXAMPLE

var grouped = _.reduce(
    properties, 
    function(buckets, property) {
        // Find the correct bucket for the property
        var bucket = _.findWhere(buckets, {size: property.size, category: property.category});

        // Create a new bucket if needed.
        if (!bucket) {
            bucket = {
                size: property.size, 
                category: property.category, 
                items: []
            };
            buckets.push(bucket);
        }

        // Add the property to the correct bucket
        bucket.items.push(property);
        return buckets;
    }, 
    [] // The starting buckets
);

console.log(grouped)

But if you just want it in an underscore mixin, here's my stab at it:

_.mixin({
'groupAndSort': function (items, sortList) {
    var grouped = _.reduce(
        items,
        function (buckets, item) {
            var searchCriteria = {};
            _.each(sortList, function (searchProperty) { searchCriteria[searchProperty] = item[searchProperty]; });
            var bucket = _.findWhere(buckets, searchCriteria);

            if (!bucket) {
                bucket = {};
                _.each(sortList, function (property) { bucket[property] = item[property]; });
                bucket._items = [];
                buckets.push(bucket);
            }

            bucket._items.push(item);
            return buckets;
        },
        [] // Initial buckets
    );

    grouped.sort(function (x, y) {
        for (var i in sortList) {
            var property = sortList[i];
            if (x[property] != y[property])
                return x[property] > y[property] ? 1 : -1;
        }
        return 0;
    });

    return _.map(grouped, function (group) {
        var toReturn = { key: {}, value: group.__items };
        _.each(sortList, function (searchProperty) { toReturn.key[searchProperty] = group[searchProperty]; });
        return toReturn;
    });
});

Upvotes: 0

Pankaj
Pankaj

Reputation: 312

How about this rather simple hack?

console.log(_.groupBy(getProperties(), function(record){
    return (record.size+record.category);
}));

Upvotes: 16

Jacob Brown
Jacob Brown

Reputation: 7561

Check out this underscore extension: Underscore.Nest, by Irene Ros.

This extension's output will be slightly different from what you specify, but the module is only about 100 lines of code, so you should be able to scan to get direction.

Upvotes: 1

Related Questions