Slapbox Slapbox - 2 months ago 8
Javascript Question

How can I merge JavaScript arrays containing objects, deduplicate and keep the newer object

I'm trying to merge two arrays of objects by checking if the titles are the same, and if they are, then checking for which entry is newer, and discarding the older one. I have found a lot of solutions for discarding true duplicates, but how can I do this in a way where I can decided which to keep based on dates?

const a = [{
"title": "title1",
"date": "2010-08-20T15:51:58"
}, {
"title": "title2",
"date": "2015-09-20T16:45:21"
}]

const b = [{
"title": "title1",
"date": "2015-08-20T15:51:58"
}, {
"title": "title2",
"date": "2015-09-20T16:45:21"
}]


Thanks for any tips you can provide!

Answer

Here is ES6 code to do that:

var res = Array.from([...a,...b].reduce ( (hash, v) =>
    !hash.has(v.title) || hash.get(v.title).date < v.date ? hash.set(v.title, v) : hash
, new Map()), v => v[1]);

var a = [{
"title": "title1",
"date": "2010-08-20T15:51:58"
}, {
"title": "title2",
"date": "2015-09-20T16:45:21"
}]

var b = [{
"title": "title1",
"date": "2015-08-20T15:51:58"
}, {
"title": "title2",
"date": "2015-09-20T16:45:21"
}]

var res = Array.from([...a,...b].reduce ( (hash, v) =>
    !hash.has(v.title) || hash.get(v.title).date < v.date ? hash.set(v.title, v) : hash
, new Map()), v => v[1]);

console.log(res);

Explanation

First the input arrays are concatenated together into one new array with the spread operator:

[...a,...b]

Then an empty Map is created and passed as last argument to reduce:

new Map()

The reduce method calls the arrow function for each element in the concatenated array. The arrow function also receives the above mentioned map as argument (as hash).

The arrow function must return a value. That value is then passed again to subsequent call of this function (for the next element), and so we always return the map, which grows in each function call. It is, as it were, passed from one call to the next. In the last call the returned map becomes the return value of .reduce().

The arrow function itself checks if the current element's title is not yet in the map:

!hash.has(v.title)

If it is in the map already, then the next expression is also evaluated; it checks whether the date in the map entry is before the current element's date.

hash.get(v.title).date < date

If either of the above conditions is true (not in map, or with smaller date), then the map entry is (re)created with the current element as value.

? hash.set(v.title, v)

This set also returns the whole map after setting. Otherwise the map is returned unchanged:

: hash

The result of reduce() is thus a map, keyed by titles. This is really the result you need, but it is in Map format. To get it back to a normal array, the Array.from method is called on it. This changes the Map values into an array of key-value pairs (sub-arrays with 2 elements). Since we are only interested in the values, we apply a function to it:

v => v[1]

This replaces every pair with only the second value. This function is passed as second argument to Array.from which applies it to every pair.

Some remarks

  • This assumes that your dates are in ISO format, like in your sample: in that case the string comparison gives the correct result for determining whether one date precedes another.

  • The result will include also the objects that only occur in one of the two input arrays

  • This is easily extendible to three input arrays: just add a third one like this: [...a,...b,...c]

  • This runs in O(n) time, where n is the total number of objects present in the input arrays. This is because most JavaScript engines implement Map access operations like .has, .get and .put with O(1) time.