2

I have an array:

[
  {
    assignmentId:17,
    email:"john.smith@email.com"
    expectation: "Make sure to proofread!",
    firstName:"John"
    id:23
    ignoreForFeedback: true
    lastName:"Smith"
    level:2
    levelFraction:null
    score:35
  },
  {
    assignmentId:17
    countsPerCategory: Array(4)
    email:"john.smith@email.com"
    firstName:"John"
    frequentErrors: Array(5)
    id:23
    ignoreForGrading: true
    lastName:"Smith"
  },
  {
    assignmentId:17,
    email:"cl@email.com"
    expectation: "cite sources",
    firstName:"Cindy"
    id:45
    ignoreForFeedback: true
    lastName:"Lee"
    level:2
    levelFraction:null
    score:32
  },
  {
    assignmentId:17
    countsPerCategory: Array(4)
    email:"cl@email.com"
    firstName:"Cindy"
    frequentErrors: Array(5)
    id:45
    ignoreForGrading: true
    lastName:"Lee"
  }
]

I want to combine the Objects with the same 'id' into the same object within the array. Their common keys should also be combined (eg: 'firstName', 'email'). Can someone suggest the best way to do this? Either with ES6 or Lodash

dedles
  • 606
  • 2
  • 8
  • 20

4 Answers4

9

You can use lodash#groupBy to group all items in the array by id and then use lodash#map with an iteratee of lodash#assign that is wrapped with a lodash#spread to make the array callback as a list of arguments for lodash#assgin.

var result = _(array)
  .groupBy('id')
  .map(_.spread(_.assign))
  .value();

var array = [
  {
    assignmentId:17,
    email:"john.smith@email.com",
    expectation: "Make sure to proofread!",
    firstName:"John",
    id:23,
    ignoreForFeedback: true,
    lastName:"Smith",
    level:2,
    levelFraction:null,
    score:35
  },
  {
    assignmentId:17,
    countsPerCategory: Array(4),
    email:"john.smith@email.com",
    firstName:"John",
    frequentErrors: Array(5),
    id:23,
    ignoreForGrading: true,
    lastName:"Smith"
  },
  {
    assignmentId:17,
    email:"cl@email.com",
    expectation: "cite sources",
    firstName:"Cindy",
    id:45,
    ignoreForFeedback: true,
    lastName:"Lee",
    level:2,
    levelFraction:null,
    score:32
  },
  {
    assignmentId:17,
    countsPerCategory: Array(4),
    email:"cl@email.com",
    firstName:"Cindy",
    frequentErrors: Array(5),
    id:45,
    ignoreForGrading: true,
    lastName:"Lee"
  }
];

var result = _(array)
  .groupBy('id')
  .map(_.spread(_.assign))
  .value();
  
console.log(result);
body > div { min-height: 100%; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

Here's an alternative solution that uses Array#filter which takes advantage of the 2nd argument of the Array#filter which gives context to the filter's callback function. We use the this context as a mechanism to store cached objects by their id and then use this to decide whether to retain these objects from the array or not.

var result = array.filter(function(v) {
  return this[v.id]?
    !Object.assign(this[v.id], v):
    (this[v.id] = v);
}, {});

var array = [
  {
    assignmentId:17,
    email:"john.smith@email.com",
    expectation: "Make sure to proofread!",
    firstName:"John",
    id:23,
    ignoreForFeedback: true,
    lastName:"Smith",
    level:2,
    levelFraction:null,
    score:35
  },
  {
    assignmentId:17,
    countsPerCategory: Array(4),
    email:"john.smith@email.com",
    firstName:"John",
    frequentErrors: Array(5),
    id:23,
    ignoreForGrading: true,
    lastName:"Smith"
  },
  {
    assignmentId:17,
    email:"cl@email.com",
    expectation: "cite sources",
    firstName:"Cindy",
    id:45,
    ignoreForFeedback: true,
    lastName:"Lee",
    level:2,
    levelFraction:null,
    score:32
  },
  {
    assignmentId:17,
    countsPerCategory: Array(4),
    email:"cl@email.com",
    firstName:"Cindy",
    frequentErrors: Array(5),
    id:45,
    ignoreForGrading: true,
    lastName:"Lee"
  }
];

var result = array.filter(function(v) {

  // does this `id` exist?
  return this[v.id]? 
  
    // assign existing object with the same id
    // from the `this` cache object. Make sure
    // to negate the resulting object with a `!`
    // to remove this value from the array
    !Object.assign(this[v.id], v):
    
    // Assign the value from the `this` cache.
    // This also retains this value from the existing
    // array
    (this[v.id] = v);
    
}, {});

console.log(result);
body > div { min-height: 100%; top: 0; }
ryeballar
  • 29,658
  • 10
  • 65
  • 74
  • @dedles Thanks, I also added a vanilla javascript as an alternative solution. If you find the overall solution to your liking, then you can mark this as the accepted answer. – ryeballar May 05 '17 at 14:44
3

You can use JavaScript's built in Array.reduce() method. The idea is you can create a map with the IDs and use lodash.merge() method (or whatever method you choose for merging objects) to merge all of the objects with the same ID into a single object. Then you can use .map() on the idMap you created to get the objects back into a single array.

    var data = [{
        assignmentId: 17,
        email: "john.smith@email.com",
        expectation: "Make sure to proofread!",
        firstName: "John",
        id: 23,
        ignoreForFeedback: true,
        lastName: "Smith",
        level: 2,
        levelFraction: null,
        score: 35
      },
      {
        assignmentId: 17,
        countsPerCategory: Array(4),
        email: "john.smith@email.com",
        firstName: "John",
        frequentErrors: Array(5),
        id: 23,
        ignoreForGrading: true,
        lastName: "Smith"
      },
      {
        assignmentId: 17,
        email: "cl@email.com",
        expectation: "cite sources",
        firstName: "Cindy",
        id: 45,
        ignoreForFeedback: true,
        lastName: "Lee",
        level: 2,
        levelFraction: null,
        score: 32
      },
      {
        assignmentId: 17,
        countsPerCategory: Array(4),
        email: "cl@email.com",
        firstName: "Cindy",
        frequentErrors: Array(5),
        id: 45,
        ignoreForGrading: true,
        lastName: "Lee"
      }
    ];

    var idMap = data.reduce(function(result, current) {
      if (result[current.id] == null) {
        result[current.id] = current;
      } else {
        _.merge(result[current.id], current);
      }

      return result;
    }, {});

    var results = Object.keys(idMap).map(function(key) {
      return idMap[key];
    });

    console.log(results);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
mhodges
  • 10,938
  • 2
  • 28
  • 46
1

What I can suggest is to use a combination of forEach() and some() methods to iterate the array elements and test if the iterated object id is already processed or not.

This is the solution:

var merged = [];

arr.forEach(function(item) {
  var idx;
  var found = merged.some(function(el, i) {
    idx = el.id === item.id ? i : null;
    return el.id === item.id;
  });
  if (!found) {
    merged.push(item);
  } else if (idx !== null) {
    for (k in Object.keys(item)) {
      if (item.hasOwnProperty(k)) {
        merged[idx][k] = item[k];
      }
    }
  }
});

Working Demo:

var arr = [{
    assignmentId: 17,
    email: "john.smith@email.com",
    expectation: "Make sure to proofread!",
    firstName: "John",
    id: 23,
    ignoreForFeedback: true,
    lastName: "Smith",
    level: 2,
    levelFraction: null,
    score: 35
  },
  {
    assignmentId: 17,
    countsPerCategory: [],
    email: "john.smith@email.com",
    firstName: "John",
    frequentErrors: [],
    id: 23,
    ignoreForGrading: true,
    lastName: "Smith"
  },
  {
    assignmentId: 17,
    email: "cl@email.com",
    expectation: "cite sources",
    firstName: "Cindy",
    id: 45,
    ignoreForFeedback: true,
    lastName: "Lee",
    level: 2,
    levelFraction: null,
    score: 32
  },
  {
    assignmentId: 17,
    countsPerCategory: [],
    email: "cl@email.com",
    firstName: "Cindy",
    frequentErrors: [],
    id: 45,
    ignoreForGrading: true,
    lastName: "Lee"
  }
];
var merged = [];

arr.forEach(function(item) {
  var idx;
  var found = merged.some(function(el, i) {
    idx = el.id === item.id ? i : null;
    return el.id === item.id;
  });
  if (!found) {
    merged.push(item);
  } else if (idx !== null) {
    for (k in Object.keys(item)) {
      if (item.hasOwnProperty(k)) {
        merged[idx][k] = item[k];
      }
    }
  }
});
console.log(merged);
cнŝdk
  • 31,391
  • 7
  • 56
  • 78
0

Thank you for your help everyone, but I ended up going with my own implementation.

        let ids = [];
        let combinedUsers = [];

        users.forEach(function (user) {
            ids.push(user.id);
        });

        ids = _.uniq(ids);
        ids.forEach(function(id){
            let user = users.filter(function(userObj){
                return id === userObj.id
            });

            if(user.length > 1){
                user = Object.assign(user[0], user[1]);
                combinedUsers.push(user);
            } else {
                combinedUsers.push(user[0]);
            }

        });
        return combinedStudents;
dedles
  • 606
  • 2
  • 8
  • 20
  • Well this is a good idea to use `.filter()` method, but your solution is mainly based on mine, you could at least upvoted it, but anyway​you shouldn't posted it as an answer, it should have been an edit to your question. :) – cнŝdk May 02 '17 at 20:31
  • Maybe great minds think alike? I had written it before I noticed your answer. I upvoted you though, for your trouble. – dedles May 02 '17 at 21:47
  • No, I just wanted to say it would be better if you posted your answer as an Edit to your question. Tha'ts it. – cнŝdk May 02 '17 at 22:40