Rod
Rod

Reputation: 15475

JavaScript - Is there a way to merge 2 objects by a specified key property?

JavaScript - Is there a way to merge 2 objects by a specified key property? The key property in this case is the authorId.

NOTE: author 3 and author 1 don't have matches, so they shouldn't get merged.

var b = [{"book": "book1", "authorId": 3},{"book": "book2", "authorId":2}];

var a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];

var c = a.merge(b);

console.log(c);

expecting:

[{"book": "book2", "authorId": 2, "author": "author2"}]

I'm trying to accomplish here what a SQL JOIN does.

https://jsfiddle.net/x67fpwoj/4/

Upvotes: 0

Views: 103

Answers (3)

Jason Cust
Jason Cust

Reputation: 10909

Assuming you need to specify the field to join on (since you mentioned SQL like syntax) and without worrying too much about performance (if it is then you would need to build an index for the object to be merged from but read the perf note below) then you can use a couple of array methods and a destructuring assignment to merge the object arrays.

const b = [{"book": "book1", "authorId": 3},{"book": "book2", "authorId":2}];

const a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];

// Using reduce will allow filtering of unmatched records
const merge = (field, x, z) => x.reduce((out, v) => {
  // Make sure the field exists in the object to merge
  // Using find will return the first object that matches the criteria (multiple record matching was not mentioned as a concern in the OP)
  const match = v.hasOwnProperty(field) && z.find((c) => c[field] === v[field]);
  
  if (match) {
    // Only push onto the output array if there is a match
    // Use destructuring assignment to merge the matching objects
    out.push({...v, ...match});
  }

  return out;
}, []);

console.log(merge('authorId', a, b));

Perf Note: Be wary with using an index approach for a variety of reasons. For instance when the object to key the merge from is very small compared to the data set to merge with. Building the index might take longer then just searching the array for the relatively small number of matches. You should use them when it makes sense but just noting that it shouldn't be a flat statement to do so.

For completeness, here is the same code with a temp index:

const b = [{"book": "book1", "authorId": 3},{"book": "book2", "authorId":2}];

const a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];

const merge = (field, x, z) => {
  const index = z.reduce((out, v) => {
    if (v.hasOwnProperty(field)) {
      out[v[field]] = v;
    }
    
    return out;
  }, {});
  
  return x.reduce((out, v) => {
    const match = index[v[field]];

    if (match) {
      out.push({...v, ...match});
    }

    return out;
  }, []);
};

console.log(merge('authorId', a, b));

Upvotes: 1

Ezzat Elbadrawy
Ezzat Elbadrawy

Reputation: 142

I think this will do what you need with clear code:

var c = [];
b.map(item1 => {
  a.map(item2 => {
    if (item1.authorId === item2.authorId) {
      c.push(Object.assign(item1, item2));
    }
  })
});
console.log(c);

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 371029

map by Object.assign from the item in the other array with the same index:

var b = [{"book": "book1", "authorId": 1},{"book": "book2", "authorId":2}];
var a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];
var c = a.map((aItem, i) => Object.assign({}, aItem, b[i]));
console.log(c);

If you can't count on each array being ordered, then you might reduce into an object indexed by authorId, and then get that object's values (this has O(N) complexity rather than a .find on each iteration, which would have O(N^2) complexity):

var a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];
var b = [{"book": "book1", "authorId": 1},{"book": "book2", "authorId":2}];

const reduceByAuthor = (arr, initial = {}) => arr.reduce((a, item) => {
  a[item.authorId] = Object.assign((a[item.authorId] || {}), item);
  return a;
}, initial);

const indexedByAuthor = reduceByAuthor(a);
reduceByAuthor(b, indexedByAuthor);
console.log(Object.values(indexedByAuthor));

If you also can't count on each item having a match in the other array, then reduce twice, with the second reduce assigning to the accumulator only if a match was found in the first indexed object (quicker than filtering afterwards):

var b = [{"book": "book1", "authorId": 3},{"book": "book2", "authorId":2}];
var a = [{"authorId": 1, "author": "author1"},{"authorId": 2, "author": "author2"}];

const aIndexed = a.reduce((a, item) => {
  a[item.authorId] = item;
  return a;
}, {});
const c = Object.values(b.reduce((a, bItem) => {
  const { authorId } = bItem;
  const aItem = aIndexed[authorId];
  if (aItem) a[authorId] = Object.assign({}, aItem, bItem);
  return a;
}, {}))

console.log(c);

Upvotes: 2

Related Questions