Grouping objects by multiple columns with Lodash or Underscore

22,995

Solution 1

A solution using underscore:

    var props = ['userId', 'replyToId'];

    var notNull = _.negate(_.isNull);

    var groups = _.groupBy(record.notes, function(note){
        return _.find(_.pick(note, props), notNull);
    });

Solution 2

This can probably done much prettier, but it should work:

lodash.mixin({
  splitGroupBy: function(list, groupByIter) {
    var _ = this, groupBy;
    if (lodash.isArray(groupByIter)) {
      groupBy = function(obj) {
        return _(obj) .pick(groupByIter)
                      .values()
                      .without(null, undefined)
                      .first();
      };
    } else {
      groupBy = groupByIter;
    }
    var groups = _.groupBy(list, groupBy);
    return groups;
  }
});

Solution 3

You can use stringify object as key.

_.groupBy(notes, ({ userId, replyToId }) => JSON.stringify({ userId, replyToId }));

output:

{
  "{\"userId\":2,\"replyToId\":null}": [
    {
      "id": 1,
      "description": "hey",
      "userId": 2,
      "replyToId": null,
      "postId": 2,
      "parentId": null
    }
  ],
  "{\"userId\":3,\"replyToId\":null}": [
    {
      "id": 5,
      "description": "hey test",
      "userId": 3,
      "replyToId": null,
      "postId": 2,
      "parentId": null
    }
  ],
  "{\"userId\":null,\"replyToId\":2}": [
    {
      "id": 2,
      "description": "how are you",
      "userId": null,
      "replyToId": 2,
      "postId": 2,
      "parentId": null,
      "user": null
    }
  ]
}

Solution 4

Assuming userId and replyToId are mutually exclusive (i.e. you either have a userId or a replyToId, but never both) as they are in the sample data, then specifying a custom grouping function works:

_.groupBy(data.notes, function(note) {
    return note.userId || note.replyToId;
});

Solution 5

You could map your list of attributes to their respective values and pick the first non falsy value as your group key:

_.mixin({
    splitGroupBy: function(list, groupByIter){
        if (!_.isArray(groupByIter))
            return _.groupBy(list, groupByIter);

        return _.groupBy(list, function(o) {
            var values = _.map(groupByIter, function(k) {
                return o[k];
            });
            return _.find(values);
        });
    }
});

var data = {  
   "notes":[  
      {  
         "id":1,
         "userId":2,
         "replyToId":null
      },
      {  
         "id":5,
         "userId":3,
         "replyToId":null
      },
      {  
         "id":2,
         "userId":null,
         "replyToId":2
      }
   ]
};

_.mixin({
    splitGroupBy: function(list, groupByIter){
        if (!_.isArray(groupByIter))
            return _.groupBy(list, groupByIter);

        return _.groupBy(list, function(o) {
            var values = _.map(groupByIter, function(k) {
                return o[k];
            });
            return _.find(values);
        });
    }
});

snippet.log(JSON.stringify(_.splitGroupBy(data.notes,['userId', 'replyToId'])));
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

<!-- Provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Share:
22,995
codebased
Author by

codebased

Amit Malhotra is a Software Engineer at Commonwealth Bank of Australia. He has an extensive experience in software architecture, design and develop, using agile approach. Amit is an expert in application development in Cloud architecture and development with Microsoft technologies such as .NET, C#, Microsoft Dynamics. He also interact with various open source frameworks such as AngularJS, EmberJS. His work involves Service Oriented Architecture (SOA) using REST, Database Design, Android technologies. Amit is Agile Certified developer. Amit has done MCA from National Institute of Electronics &amp; Information Technology (NIELIT), (erstwhile DOEACC Society). Twit him on @imcodebased

Updated on July 12, 2022

Comments

  • codebased
    codebased almost 2 years

    I have following object records:

     {  
       "notes":[  
          {  
             "id":1,
             "description":"hey",
             "userId":2,
             "replyToId":null,
             "postId":2,
             "parentId":null
          },
          {  
             "id":5,
             "description":"hey test",
             "userId":3,
             "replyToId":null,
             "postId":2,
             "parentId":null
          },
          {  
             "id":2,
             "description":"how are you",
             "userId":null,
             "replyToId":2,
             "postId":2,
             "parentId":null,
             "user":null
          }
       ]
    }
    

    I want to output it as:

    2 
      object with id 1
      object with id 2 (because replyToId value is same as userId
    3
      object with id 5
    

    So basically I want to consider UserId and replyToId value under the same group.

    I have build my own mixin under lodash, wrapping groupBy method as:

    mixin({
        splitGroupBy: function(list, groupByIter){
            if (_.isArray(groupByIter)) {
                function groupBy(obj) {
                    return _.forEach(groupByIter, function (key){
                        if ( !!obj[key] ) return obj[key]
                    });
    
                }
            } else {
                var groupBy = groupByIter;
            }
    
            debugger;
    
            var groups = _.groupBy(list, groupBy);
    
            return groups;
        }
    });
    

    Call looks like this:

    _.splitGroupBy(data.notes,['userId', 'replyToId']);
    

    The output is coming without group. Even when I have tried with _.map instead _.forEach the split is not happening correctly.