[javascript] Merge two array of objects based on a key

I have two arrays:

Array 1:

[
  { id: "abdc4051", date: "2017-01-24" }, 
  { id: "abdc4052", date: "2017-01-22" }
]

and array 2:

[
  { id: "abdc4051", name: "ab" },
  { id: "abdc4052", name: "abc" }
]

I need to merge these two arrays based on id and get this:

[
  { id: "abdc4051", date: "2017-01-24", name: "ab" },
  { id: "abdc4052", date: "2017-01-22", name: "abc" }
]

How can I do this without iterating trough Object.keys?

This question is related to javascript

The answer is


We can use lodash here. _.merge works as you expected. It works with the common key present.

_.merge(array1, array2)

You could use an arbitrary count of arrays and map on the same index new objects.

_x000D_
_x000D_
var array1 = [{ id: "abdc4051", date: "2017-01-24" }, { id: "abdc4052", date: "2017-01-22" }],_x000D_
    array2 = [{ id: "abdc4051", name: "ab" }, { id: "abdc4052", name: "abc" }],_x000D_
    result = [array1, array2].reduce((a, b) => a.map((c, i) => Object.assign({}, c, b[i])));_x000D_
    _x000D_
console.log(result);
_x000D_
.as-console-wrapper { max-height: 100% !important; top: 0; }
_x000D_
_x000D_
_x000D_


This is a version when you have an object and an array and you want to merge them and give the array a key value so it fits into the object nicely.

_x000D_
_x000D_
var fileData = [_x000D_
    { "id" : "1", "filename" : "myfile1", "score" : 33.1 }, _x000D_
    { "id" : "2", "filename" : "myfile2", "score" : 31.4 }, _x000D_
    { "id" : "3", "filename" : "myfile3", "score" : 36.3 }, _x000D_
    { "id" : "4", "filename" : "myfile4", "score" : 23.9 }_x000D_
];_x000D_
_x000D_
var fileQuality = [0.23456543,0.13413131,0.1941344,0.7854522];_x000D_
_x000D_
var newOjbect = fileData.map((item, i) => Object.assign({}, item, {fileQuality:fileQuality[i]}));_x000D_
_x000D_
console.log(newOjbect);
_x000D_
_x000D_
_x000D_


You can do this in one line

_x000D_
_x000D_
let arr1 = [_x000D_
    { id: "abdc4051", date: "2017-01-24" },_x000D_
    { id: "abdc4052", date: "2017-01-22" }_x000D_
];_x000D_
_x000D_
let arr2 = [_x000D_
    { id: "abdc4051", name: "ab" },_x000D_
    { id: "abdc4052", name: "abc" }_x000D_
];_x000D_
_x000D_
const mergeById = (a1, a2) =>_x000D_
    a1.map(itm => ({_x000D_
        ...a2.find((item) => (item.id === itm.id) && item),_x000D_
        ...itm_x000D_
    }));_x000D_
_x000D_
console.log(mergeById(arr1, arr2));
_x000D_
_x000D_
_x000D_

  1. Map over array1
  2. Search through array2 for array1.id
  3. If you find it ...spread the result of array2 into array1

The final array will only contain id's that match from both arrays


This solution is applicable even when you have different size of array being merged. Also, even if the keys on which match is happening has a different name.

const arr1 = [
  { id: "abdc4051", date: "2017-01-24" }, 
  { id: "abdc4052", date: "2017-01-22" },
  { id: "abdc4053", date: "2017-01-22" }
];
const arr2 = [
  { nameId: "abdc4051", name: "ab" },
  { nameId: "abdc4052", name: "abc" }
];

Now to merge these use a Map as follows:

const map = new Map();
arr1.forEach(item => map.set(item.id, item));
arr2.forEach(item => map.set(item.nameId, {...map.get(item.nameId), ...item}));
const mergedArr = Array.from(map.values());

This should result in:

[
  {
    "id": "abdc4051",
    "date": "2017-01-24",
    "nameId": "abdc4051",
    "name": "ab"
  },
  {
    "id": "abdc4052",
    "date": "2017-01-22",
    "nameId": "abdc4052",
    "name": "abc"
  },
  {
    "id": "abdc4053",
    "date": "2017-01-22"
  }
]

Here's an O(n) solution using reduce and Object.assign

const joinById = ( ...lists ) =>
    Object.values(
        lists.reduce(
            ( idx, list ) => {
                list.forEach( ( recod ) => {
                    if( idx[ recod.id ] )
                        idx[ recod.id ] = Object.assign( idx[ recod.id ], recod )
                    else
                        idx[ recod.id ] = recod
                } )
                return idx
            },
            {}
        )
    )

Each list gets reduced to a single object where the keys are ids and the values are the objects. If there's a value at the given key already, it gets object.assign called on it and the current record.

Here's the generic O(n*m) solution, where n is the number of records and m is the number of keys. This will only work for valid object keys. You can convert any value to base64 and use that if you need to.

const join = ( keys, ...lists ) =>
    lists.reduce(
        ( res, list ) => {
            list.forEach( ( record ) => {
                let hasNode = keys.reduce(
                    ( idx, key ) => idx && idx[ record[ key ] ],
                    res[ 0 ].tree
                )
                if( hasNode ) {
                    const i = hasNode.i
                    Object.assign( res[ i ].value, record )
                    res[ i ].found++
                } else {
                    let node = keys.reduce( ( idx, key ) => {
                        if( idx[ record[ key ] ] )
                            return idx[ record[ key ] ]
                        else
                            idx[ record[ key ] ] = {}
                        return idx[ record[ key ] ]
                    }, res[ 0 ].tree )
                    node.i = res[ 0 ].i++
                    res[ node.i ] = {
                        found: 1,
                        value: record
                    }
                }
            } )
            return res
        },
        [ { i: 1, tree: {} } ]
         )
         .slice( 1 )
         .filter( node => node.found === lists.length )
         .map( n => n.value )

This is essentially the same as the joinById method, except that it keeps an index object to identify records to join. The records are stored in an array and the index stores the position of the record for the given key set and the number of lists it's been found in.

Each time the same key set is encountered, the node is found in the tree, the element at it's index is updated, and the number of times it's been found is incremented.

finally, the idx object is removed from the array with the slice, any elements that weren't found in each set are removed. This makes it an inner join, you could remove this filter and have a full outer join.

finally each element is mapped to it's value, and you have the merged array.


To merge the two arrays on id, assuming the arrays are equal length:

arr1.map(item => ({
    ...item,
    ...arr2.find(({ id }) => id === item.id),
}));

Non of these solutions worked for my case:

  • missing objects can exist in either array
  • runtime complexity of O(n)

notes:

  • I used lodash but it's easy to replace with something else
  • Also used Typescript (just remove/ignore the types)
import { keyBy, values } from 'lodash';

interface IStringTMap<T> {
  [key: string]: T;
}

type IIdentified = {
  id?: string | number;
};

export function mergeArrayById<T extends IIdentified>(
  array1: T[],
  array2: T[]
): T[] {
  const mergedObjectMap: IStringTMap<T> = keyBy(array1, 'id');

  const finalArray: T[] = [];

  for (const object of array2) {
    if (object.id && mergedObjectMap[object.id]) {
      mergedObjectMap[object.id] = {
        ...mergedObjectMap[object.id],
        ...object,
      };
    } else {
      finalArray.push(object);
    }
  }

  values(mergedObjectMap).forEach(object => {
    finalArray.push(object);
  });

  return finalArray;
}

You can recursively merge them into one as follows:

_x000D_
_x000D_
function mergeRecursive(obj1, obj2) {_x000D_
    for (var p in obj2) {_x000D_
        try {_x000D_
            // Property in destination object set; update its value._x000D_
            if (obj2[p].constructor == Object) {_x000D_
                obj1[p] = this.mergeRecursive(obj1[p], obj2[p]);_x000D_
_x000D_
            } else {_x000D_
                obj1[p] = obj2[p];_x000D_
_x000D_
            }_x000D_
_x000D_
        } catch (e) {_x000D_
            obj1[p] = obj2[p];_x000D_
_x000D_
        }_x000D_
    }_x000D_
    return obj1;_x000D_
}_x000D_
_x000D_
arr1 = [_x000D_
    { id: "abdc4051", date: "2017-01-24" },_x000D_
    { id: "abdc4052", date: "2017-01-22" }_x000D_
];_x000D_
arr2 = [_x000D_
    { id: "abdc4051", name: "ab" },_x000D_
    { id: "abdc4052", name: "abc" }_x000D_
];_x000D_
_x000D_
mergeRecursive(arr1, arr2)_x000D_
console.log(JSON.stringify(arr1))
_x000D_
_x000D_
_x000D_


If you have 2 arrays need to be merged based on values even its in different order

let arr1 = [
    { id:"1", value:"this", other: "that" },
    { id:"2", value:"this", other: "that" }
];

let arr2 = [
    { id:"2", key:"val2"},
    { id:"1", key:"val1"}
];

you can do like this

const result = arr1.map(item => {
    const obj = arr2.find(o => o.id === item.id);
    return { ...item, ...obj };
  });

console.log(result);

You can use array methods

_x000D_
_x000D_
let arrayA=[_x000D_
{id: "abdc4051", date: "2017-01-24"},_x000D_
{id: "abdc4052", date: "2017-01-22"}]_x000D_
_x000D_
let arrayB=[_x000D_
{id: "abdc4051", name: "ab"},_x000D_
{id: "abdc4052", name: "abc"}]_x000D_
_x000D_
let arrayC = [];_x000D_
_x000D_
_x000D_
function isBiggerThan10(element, index, array) {_x000D_
  return element > 10;_x000D_
}_x000D_
_x000D_
arrayA.forEach(function(element){_x000D_
  arrayC.push({_x000D_
  id:element.id,_x000D_
  date:element.date,_x000D_
  name:(arrayB.find(e=>e.id===element.id)).name_x000D_
  });  _x000D_
});_x000D_
_x000D_
console.log(arrayC);_x000D_
_x000D_
//0:{id: "abdc4051", date: "2017-01-24", name: "ab"}_x000D_
//1:{id: "abdc4052", date: "2017-01-22", name: "abc"}
_x000D_
_x000D_
_x000D_


Well... assuming both arrays are of the same length, I would probably do something like this:

var newArr = []
for (var i = 0; i < array1.length; i++ {
    if (array1[i].id === array2[i].id) {
      newArr.push({id: array1[i].id, date: array1[i].date, name: array2[i].name});
  }
}

I was able to achieve this with a nested mapping of the two arrays and updating the initial array:

member.map(mem => {
return memberInfo.map(info => {
    if (info.id === mem.userId) {
        mem.date = info.date;
        return mem;
        }
    }
}