2

I'm trying to manipulate this sample array of objects.

[ { name: 'John Wilson',
    id: 123,
    classes: ['java', 'c++']},
  { name: 'John Wilson',
    id: 123,
    classes: 'uml'},
   { name: 'Jane Smith',
    id: 321,
    classes: 'c++'} ]

What I need to do is to merge objects with the same 'id', concatenating 'classes' and keeping one 'name'.

The result should be:

[ { name: 'John Wilson',
    id: 123,
    classes: ['java', 'c++', 'uml']},
   { name: 'Jane Smith',
    id: 321,
    classes: 'c++'} ]

I tried using .merge but it doesn't concatenate the values from 'classes', it just keeps the values from the last equal object.

What is the simplest way to do that, using lodash?

5 Answers 5

3

The function you're looking for is _.uniqWith, with a special twist which I will explain in a minute.

_.uniqWith is a lot like _.uniq in that it generates a unique array, but it allows you to pass your own custom comparator function that will be called to determine what counts as "equality."

Sane programmers would understand that this comparator should be side-effect free. The way this code works is by breaking that rule, and using a comparison function that does extra magic behind the scenes. However, this results in very concise code that will work no matter how many of these objects are in your array, so I feel like the transgression is well-justified.

I named the comparator function compareAndMerge so as not to hide its impure nature. It will merge both classes arrays and update the relevant property on both objects, but only if their id values are identical.

function merge(people) {
  return _.uniqWith(people, compareAndMerge)
}

function compareAndMerge(first, second) {
    if (first.id === second.id) {
        first.classes = second.classes = [].concat(first.classes, second.classes)
        return true
    }
    return false
}


var people = [{
  name: 'John Wilson',
  id: 123,
  classes: ['java', 'c++']
}, {
  name: 'John Wilson',
  id: 123,
  classes: 'uml'
}, {
  name: 'Jane Smith',
  id: 321,
  classes: 'c++'
}]

console.log(merge(people))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>

An aside: You were missing square brackets around your original classes lists. I made sure that the code above doesn't care whether or not the classes property holds a single string or an array of strings, though, just in case.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you! I actually forgot about the brackets. The code is clean and works great.
Glad to have helped! I learned quite a bit about lodash along the way, too.
2

Using ES6 you can do so with a Map to hold the unique values, Array#reduce to populate it, and the spread operator with Map#values to convert it back to array:

const arr = [{"name":"John Wilson","id":123,"classes":["java","c++"]},{"name":"John Wilson","id":123,"classes":"uml"},{"name":"Jane Smith","id":321,"classes":"c++"}];

const result = [...arr.reduce((hash, { id, name, classes }) => {
  const current = hash.get(id) || { id, name, classes: [] };
  
  classes && (current.classes = current.classes.concat(classes));
  
  return hash.set(id, current);
}, new Map).values()];

console.log(result);

1 Comment

This is really clever and quite creative. +1 for knowing ES6 inside and out.
0

Not sure using lodash... here's a way to do it with normal JS:

var combined = arr.reduce(function(a, item, idx) {
    var found = false;
    for (var i = 0; i < a.length; i++) {
        if (a[i].id == item.id) {
            a[i].classes = a[i].classes.concat(item.classes);
            found = true;
            break;
        }
    }

    if (!found) {
        a.push(item);
    }

    return a;
}, []);

Fiddle: https://jsfiddle.net/6zwr47mt/

Comments

0

use _.mergeWith to set merging customizer

_.reduce(data, function(result, item) {
    item = _.mergeWith(
        item,
        _.find(result, {id: item.id}),
        function(val, addVal) {
            return _.isArray(val) ? _.concat(val, addVal) : val;
        });
    result = _.reject(result, {id: item.id})
    return _.concat(result, item);
}, []);

Comments

0

The following algorithm is not the best one but at least I know what it does :-)

console.log(clean(data));

function clean (data) {
  var i, x, y;
  var clean = [];
  var m = clean.length;
  var n = data.length;
  data.sort((x, y) => x.id - y.id);
  for (i = 0; i < n; i++) {
    y = data[i];
    if (i == 0 || x.id != y.id) {
      clean.push(x = clone(y)), m++;
    } else {
      clean[m - 1] = merge(x, y);
    }
  }
  return clean;
}

function clone (x) {
  var z = {};
  z.id = x.id;
  z.name = x.name;
  z.classes = x.classes.slice();
  return z;
}

function merge (x, y) {
  var z = {};
  z.id = x.id;
  z.name = x.name;
  z.classes = unique(
    x.classes.concat(y.classes)
  );
  return z;
}

function unique (xs) {
  var i, j, n;
  n = xs.length;
  for (i = 1; i < n; i++) {
    j = 0; while (j < i && xs[i] !== xs[j]) j++;
    if (j < i) swap(xs, i, n - 1), i--, n--;
  }
  return xs.slice(0, n);
}

function swap (xs, i, j) {
  var x = xs[i];
  xs[i] = xs[j];
  xs[j] = x;
}
<script>
  var data = [{
    id: 123,
    name: 'John Wilson',
    classes: ['java', 'c++']
  }, {
    id: 123,
    name: 'John Wilson',
    classes: ['uml', 'java']
  }, {
    id: 321,
    name: 'Jane Smith',
    classes: ['c++']
  }];
</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.