sprucegoose
sprucegoose

Reputation: 610

merge objects from two arrays depending on multiple keys

I have two arrays, below, which I'd like to merge based on two common keys ("iso3" and "year"). Each object must have both in common in order to be merged. The arrays are not the same length.

array1 = [{
  "id":24006,
  "iso3":"AFG",
  "country":"Afghanistan",
  "year":2014,
  "value":29.78
},
{
  "id":138806,
  "iso3":"ALB",
  "country":"Albania",
  "year":2013,
  "value":0.6341109715
},
{
  "id":44206,
  "iso3":"DZA",
  "country":"Algeria",
  "year":2014,
  "value":39.928947
}]

array2 = [{
  "indicator_id":21806,
  "footnote_id":64811,
  "iso3":"AFG",
  "year":2014
},
{
  "indicator_id":23806,
  "footnote_id":15711,
  "iso3":"AFG",
  "year":2013
},
{
  "indicator_id":123406,
  "footnote_id":15711,
  "iso3":"ALB",
  "year":2013
},
{
  "indicator_id":101606,
  "footnote_id":48911,
  "iso3":"DZA",
  "year":2013
}];

I saw this answer, but the arrays are merged based on only one common key. I tried to work from that code, but I didn't get anywhere. Any help is appreciated!

Upvotes: 1

Views: 693

Answers (4)

Nina Scholz
Nina Scholz

Reputation: 386578

This solution features an object for collecting items and renders then the wanted array.

var array1 = [{ "id": 24006, "iso3": "AFG", "country": "Afghanistan", "year": 2014, "value": 29.78 }, { "id": 138806, "iso3": "ALB", "country": "Albania", "year": 2013, "value": 0.6341109715 }, { "id": 44206, "iso3": "DZA", "country": "Algeria", "year": 2014, "value": 39.928947 }],
    array2 = [{ "indicator_id": 21806, "footnote_id": 64811, "iso3": "AFG", "year": 2014 }, { "indicator_id": 23806, "footnote_id": 15711, "iso3": "AFG", "year": 2013 }, { "indicator_id": 123406, "footnote_id": 15711, "iso3": "ALB", "year": 2013 }, { "indicator_id": 101606, "footnote_id": 48911, "iso3": "DZA", "year": 2013 }];

function merge(array1, array2) {

    function makeObj(a) {
        obj[a.iso3] = obj[a.iso3] || {};
        obj[a.iso3][a.year] = obj[a.iso3][a.year] || {};
        Object.keys(a).forEach(function (k) {
            obj[a.iso3][a.year][k] = a[k];
        });
    }
    var array = [],
        obj = {};

    array1.forEach(makeObj);
    array2.forEach(makeObj);
    Object.keys(obj).forEach(function (k) {
        Object.keys(obj[k]).forEach(function (kk) {
            array.push(obj[k][kk]);
        });
    });
    return array;
}

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

Upvotes: 0

Simon Campbell
Simon Campbell

Reputation: 752

This approach first creates a dictionary (or map) of the merged objects based on the key. It uses jQuery $.extend to merge the individual objects (other utility libraries will likely provide a means to merge objects)

After producing the dictionary it then transform back into an array and filters by individual object keys to produce an inner join like result. Simply remove the filter if you are after an outer join.

var array1 = [{
  "id":24006,
  "iso3":"AFG",
  "country":"Afghanistan",
  "year":2014,
  "value":29.78
},
{
  "id":138806,
  "iso3":"ALB",
  "country":"Albania",
  "year":2013,
  "value":0.6341109715
},
{
  "id":44206,
  "iso3":"DZA",
  "country":"Algeria",
  "year":2014,
  "value":39.928947
}]

var array2 = [{
  "indicator_id":21806,
  "footnote_id":64811,
  "iso3":"AFG",
  "year":2014
},
{
  "indicator_id":23806,
  "footnote_id":15711,
  "iso3":"AFG",
  "year":2013
},
{
  "indicator_id":123406,
  "footnote_id":15711,
  "iso3":"ALB",
  "year":2013
},
{
  "indicator_id":101606,
  "footnote_id":48911,
  "iso3":"DZA",
  "year":2013
}];

// merge by key where possible
var dict = array1.concat(array2).reduce(function (dict, val) {
    // Use the two values we want to join on as a key
      var key = val.iso3 + val.year;

    // Lookup existing object with same key, default to
    // empty object if one does not exist
    var existing = dict[key] || {};

    // Merge two objects
    dict[key] = $.extend(existing, val);

    return dict; 
}, {});

var joined = Object.keys(dict).map(function (key) {
  // pull the dictionary values back out into an array
  return dict[key];
}).filter(function (obj) {
  // this is making it work like an inner out, an outer join can be
  // done by removing the filter
  return (typeof obj.indicator_id !== "undefined") && (typeof obj.country !== "undefined");
});

JS Fiddle (output is logged to console: http://jsfiddle.net/9v47zLy6/)

Upvotes: 0

kamituel
kamituel

Reputation: 35950

Another approach that utilizes sort() and reduce().

JSFiddle example.

Code:

/* Merges two objects into one. You might add some custom logic here,
   if you need to handle some more complex merges, i.e. if the same
   key appears in merged objects in both arrays */
var merge = function(o1, o2) {
  var result = {};
  for (var a in o1) { result[a] = o1[a]; }
  for (var a in o2) { result[a] = o2[a]; }
  return result;
}

/* Returns 0 if two objects are equal, -1 if first one is 'lower',
   and 1 if it's 'larger'. It's used to sort the arrays. Objects are
   compared using iso3 and year properties only. */
var compareByIsoAndYear = function (o1, o2) {
  if (o1.iso3 === o2.iso3 && o1.year === o2.year) return 0
  else if (o1.year > o2.year) return 1
  else if (o1.year < o2.year) return -1
  else if (o1.iso3 > o2.iso3) return 1;
}

/* Used in reduce */
var mergeTwoObjects = function(initial, current) {
  if (initial.length > 0) {
    var last = initial[initial.length-1];
    if (compareByIsoAndYear(last, current) === 0) {
      initial[initial.length-1] = merge(last, current);
      return initial;
    }
  }
  initial.push(current);
  return initial;
}

/* Take both arrays and concatenate them into one. Then sort them,
   to make sure that objects with the same year and iso3 would appear
   next to each other, and then merge them if needed */
var result = array1.concat(array2)
                   .sort(compareByIsoAndYear)
                   .reduce(mergeTwoObjects, []);

Result:

[{
  "indicator_id": 23806,      /* Found in array2 only, not merged */
  "footnote_id": 15711,
  "iso3": "AFG",
  "year": 2013
}, {
  "id": 138806,               /* This one has been merged */
  "iso3": "ALB",
  "country": "Albania",
  "year": 2013,
  "value": 0.6341109715,
  "indicator_id": 123406,
  "footnote_id": 15711
}, {           
  "indicator_id": 101606,     /* Found in array2 only, not merged */
  "footnote_id": 48911,
  "iso3": "DZA",
  "year": 2013
}, {
  "id": 24006,                /* This one has been merged */
  "iso3": "AFG",
  "country": "Afghanistan",
  "year": 2014,
  "value": 29.78,
  "indicator_id": 21806,
  "footnote_id": 64811
}, {
  "id": 44206,                /* Found in array1 only, not merged */
  "iso3": "DZA",
  "country": "Algeria",
  "year": 2014,
  "value": 39.928947
}]

Upvotes: 3

Alex Kirko
Alex Kirko

Reputation: 466

What you need to do is extend the joining condition offered in the answer you sited. Like this:

var combined = [];
function findSecond(iso3, year, second){
    for (var i = 0; i < second.length; i += 1){
    //Adding the second condition
        if (second[i].iso3 === iso3 && second[i].year ===  year ) {
            return second[i];
        }
    }
    return null;
}

while (el = array1.pop()){
    var getSec = findSecond(el.iso3, el.year, array2);
    if (getSec){
        for (var l in getSec){
            if (!(l in el)) {
                el[l] = getSec[l];
            }
        }
        combined.push(el);
    }
}

Upvotes: 0

Related Questions