R. Favero
R. Favero

Reputation: 33

Merge objects concatenating values, using lodash

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?

Upvotes: 2

Views: 4172

Answers (5)

Ori Drori
Ori Drori

Reputation: 191976

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);

Upvotes: 2

gyre
gyre

Reputation: 16777

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.

Upvotes: 3

user1636522
user1636522

Reputation:

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>

Upvotes: 0

stasovlas
stasovlas

Reputation: 7416

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);
}, []);

Upvotes: 0

tymeJV
tymeJV

Reputation: 104775

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/

Upvotes: 0

Related Questions