Sean O
Sean O

Reputation: 2619

JavaScript - merge two arrays of objects and de-duplicate based on property value

I want to update (replace) the objects in my array with the objects in another array. Each object has the same structure. e.g.

var origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];
var updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];
// desired result:
NEWArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

I've tried concat() & Underscore's _.uniq function, but it always dumps the newer object & returns, essentially, the original array.

Is there a way to overwrite (replace) origArr with the objects in updatingArr -- matching on the name property?

Upvotes: 20

Views: 58797

Answers (14)

Sourabh Khurana
Sourabh Khurana

Reputation: 71

const origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];

const updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

let hash = {};

for(let i of origArr.concat(updatingArr)) {
  if(!hash[i.name]) {
    hash[i.name] = i;
  }
}

let newArr = Object.values(hash);


console.log(newArr);

Upvotes: 3

Nina Scholz
Nina Scholz

Reputation: 386883

You could use Array#map in combination with Array#reduce

var origArr = [{ name: 'Trump', isRunning: true }, { name: 'Cruz', isRunning: true }, { name: 'Kasich', isRunning: true }],
    updatingArr = [{ name: 'Cruz', isRunning: false }, { name: 'Kasich', isRunning: false }],
    NEWArr = origArr.map(function (a) {
        return this[a.name] || a;
    }, updatingArr.reduce(function (r, a) {
        r[a.name] = a;
        return r;
    }, Object.create(null)));

document.write('<pre>' + JSON.stringify(NEWArr, 0, 4) + '</pre>');

UPDATE 2022

Using an object with name as hash and mapping the original array by taking the update from hash table or the original object.

const
    origArr = [{ name: 'Trump', isRunning: true }, { name: 'Cruz', isRunning: true }, { name: 'Kasich', isRunning: true }],
    updatingArr = [{ name: 'Cruz', isRunning: false }, { name: 'Kasich', isRunning: false }],
    updates = Object.fromEntries(updatingArr.map(o => [o.name, o])),
    result = origArr.map(o => updates[o.name] || o);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Another approach by using Map.

This approach works for objects who are only in the updating array as well.

const
    origArr = [{ name: 'Trump', isRunning: true }, { name: 'Cruz', isRunning: true }, { name: 'Kasich', isRunning: true }],
    updatingArr = [{ name: 'Cruz', isRunning: false }, { name: 'Kasich', isRunning: false }],
    result = Array.from([...origArr, ...updatingArr]
        .reduce((m, o) => m.set(o.name, o), new Map)
        .values()
    );

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 10

Julian Gonggrijp
Julian Gonggrijp

Reputation: 4380

While Underscore does not have a direct equivalent to Lodash's unionBy, you can still get the same result in an efficient oneliner using concat, groupBy, map and last:

import { groupBy, map, last } from 'underscore';

var NEWArr = map(groupBy(origArr.concat(updateArr), 'name'), last);

Equivalent, slightly lengthier but cleaner notation using chain:

import { chain } from 'underscore';

var NEWArr = chain(origArr).concat(updateArr).groupBy('name').map(last).value();

In fact, you can define your own unionBy using the above recipe and add it to Underscore yourself:

import _, { groupBy, map, first } from 'underscore';

function unionBy(...args) {
    const iteratee = _.iteratee(args.pop());
    const candidates = [].concat.apply(null, args);
    return map(groupBy(candidates, iteratee), first);
}

_.mixin({ unionBy }); // unionBy can now be used in chaining

Note that we are now using first instead of last, because the semantics of Lodash's unionBy dictate that the first occurrence wins.

Upvotes: 0

Julian Gonggrijp
Julian Gonggrijp

Reputation: 4380

Backbone's collections are well equipped for scenarios like this one.

First, we define a model type that knows to look for the name property:

import { Model } from 'backbone';

const President = Model.extend({ idAttribute: 'name' });

Then, we put the initial array in a collection that uses the above model:

import { Collection } from 'backbone';

const presidents = new Collection(origArr, { model: President });

Now, we can run presidents.set as often as we want, and it will merge the new representation with the old values:

presidents.set(updateArr);

presidents.get('Cruz').get('isRunning'); // false
presidents.toJSON(); // NEWArr

Putting it all together in a runnable snippet:

const { Model, Collection } = Backbone;

const President = Model.extend({ idAttribute: 'name' });

const presidents = new Collection([
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
], { model: President });

presidents.set([
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
]);

console.log(presidents.get('Cruz').get('isRunning'));
console.log(presidents.toJSON());
<script src="https://underscorejs.org/underscore-umd-min.js"></script>
<script src="https://backbonejs.org/backbone-min.js"></script>

Upvotes: 0

mariosantos
mariosantos

Reputation: 69

In ES6 you can use the object Map like this...

let map = new Map();
let origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];
let updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

// Concating arrays with duplicates
let NEWArr = origArr.concat(updatingArr);

// Removing duplicates items
NEWArr.forEach(item => {
  if(!map.has(item.name)){
    map.set(item.name, item);
  }
});

Array.from(map.values());

Remember: the Map object need in unique key, in this case I used the name.

Upvotes: 2

Vincent Tang
Vincent Tang

Reputation: 4170

Same as @gevorg answer, but you may want to also add a new object to the original array if no matches are found.

let combinedEvents = origEvents;
for(let i =0; i< newEvents.length; i++){
  let newEvent = newEvents[i];
  for(let j =0; j< origEvents.length; j++){
    let origEvent = origEvents[j];
    if(newEvent.events_id == origEvent.events_id){
      combinedEvents.splice(j,1, newEvent);
      break;
    } else if(j === origEvents.length - 1){
      combinedEvents.push(newEvent);
      break;
    }
  }
}

Upvotes: 1

Saquib Akhter
Saquib Akhter

Reputation: 160

Try this approach with ES-6 Set Data Structure: const result = [...new Set([...origArr, ...updatingArr])]

Upvotes: -1

aquiseb
aquiseb

Reputation: 1009

This version lets you define the selector that defines an object as duplicate.

  • forEach iterates over the new data
  • findIndex returns an index >= 0 if two selectors are equal. If none are equal, it returns -1
  • If there is a duplicate, we use slice to replace the original by the new.
  • If there's no duplicate, we push it into the original array.

const origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];

const updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

const mergeArrayOfObjects = (original, newdata, selector = 'key') => {
	newdata.forEach(dat => {
		const foundIndex = original.findIndex(ori => ori[selector] == dat[selector]);
		if (foundIndex >= 0) original.splice(foundIndex, 1, dat);
        else original.push(dat);
	});

	return original;
};

const result = mergeArrayOfObjects(origArr, updatingArr, "name")
console.log('RESULT -->', result)

Upvotes: 2

Martin Bramwell
Martin Bramwell

Reputation: 2121

I came here looking for exactly this, saw @Gruff Bunny 's technique and wondered if 'lodash' wouldn't perhaps be a superior option even to 'underscore'?

Lo and behold :

let result = _.unionBy(updatingArr, origArr, 'name');

Upvotes: 21

spaceman
spaceman

Reputation: 1167

Using a double for loop and splice you can do it like so:

for(var i = 0, l = origArr.length; i < l; i++) {
    for(var j = 0, ll = updatingArr.length; j < ll; j++) {
        if(origArr[i].name === updatingArr[j].name) {
            origArr.splice(i, 1, updatingArr[j]);
            break;
        }
    }
}

Example here

Upvotes: 9

Gruff Bunny
Gruff Bunny

Reputation: 27986

Here's a solution using underscore:

var result = _.map(origArr, function(orig){
    return _.extend(orig, _.findWhere(updatingArr, {name: orig.name}));
});

Upvotes: 2

Manish M Demblani
Manish M Demblani

Reputation: 910

You can give this a try.

var origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];
var updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

var origLength = origArr.length;
var updatingLength = updatingArr.length;

//Traverse the original array and replace only if the second array also has the same value
for(i = origLength-1; i >= 0; i--) {
    for(j = updatingLength -1; j >= 0; j--) {
    if(origArr[i].name === updatingArr[j].name) {
        origArr[i] = updatingArr[j];
    }
  }
}

console.log(origArr);

Upvotes: 2

Oriol
Oriol

Reputation: 288710

You can use a hash which gives the index by name, and Object.assign to update.

var hash = origArr.reduce(function(hash, obj, index) {
  hash[obj.name] = index;
  return hash;
}, Object.create(null));
for(var obj of updatingArr) {
  Object.assign(origArr[hash[obj.name]], obj);
}

Upvotes: 2

gevorg
gevorg

Reputation: 5065

This will do what you need:

var origArr = [
  {name: 'Trump', isRunning: true},
  {name: 'Cruz', isRunning: true},
  {name: 'Kasich', isRunning: true}
];

var updatingArr = [
  {name: 'Cruz', isRunning: false},
  {name: 'Kasich', isRunning: false}
];

for (var i = 0; i < updatingArr.length; ++i) {
  var updateItem = updatingArr[i];
  for (var j = 0; j < origArr.length; ++j) {
    var origItem = origArr[j];
    if (origItem.name == updateItem.name) {
      origItem.isRunning = updateItem.isRunning;
      break;    
    }
  }
}

document.write('<pre>' + JSON.stringify(origArr, 0, 4) + '</pre>');

Upvotes: 1

Related Questions