4

I would like to merge 2 objects with the same properties into an Array.

Take this for an example:

object1 = {"id":1,
           "name":name1,
           "children":[{"id":2,"name":name2}]
          };
object2 = {"id":3,
           "name":name3,
           "children":[{"id":4,"name":name4}]
          };
object3 = {"id":1,
           "name":name1,
           "children":[{"id":6,"name":name6}]
          };
var result = Object.assign(result,object1,object2,object3);

Expected result:

JSON.stringify([result]) =[
                           {"id":1,
                            "name":name1,
                            "children":[{"id":2,"name":name2},
                                       {"id":6,"name":name6}]
                           },
                           {"id":3,
                            "name":name3,
                            "children":[{"id":4,"name":name4}]
                           }
                          ]

Actual result:

JSON.stringify([result]) = [
                            {"id":3,
                             "name":name3,
                             "children":[{"id":4,"name":name4}]
                            }
                           ]

Seems like Object.assign() isn't the way to go... as it will overwrite, I do not want it to overwrite, I want them to merge instead. Is there a right way to do this?

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Ronaldo
  • 263
  • 5
  • 18
  • so, it's not really an "opposite" you're looking for - there's so many examples of how to do this just on stack overflow, really – Jaromanda X Oct 27 '17 at 08:59
  • @JaromandaX Thanks for correcting my title... yes is there any better way to do this than use Object.assign()? – Ronaldo Oct 27 '17 at 09:01
  • Object.assign is for objects, what your after is merging array's. Or even simply, just adding to array. – Keith Oct 27 '17 at 09:02
  • since Object assign doesn't do what you want, then I suspect there is a better way - probably about 3 or 4 distinct ways – Jaromanda X Oct 27 '17 at 09:03
  • your expected result is impossible, because you expect `JSON.stringify([result])` to be an array of Objects, that would require `result` to be multiple Objects - clearly impossible – Jaromanda X Oct 27 '17 at 09:11

8 Answers8

6

As so often, Array.prototype.reduce provides a good base for an approach like e.g. this one ...

var obj1 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 2, "name": "name2" }]
};
var obj2 = {
  "id": 3,
  "name": "name3",
  "children": [{ "id": 4, "name": "name4" }]
};
var obj3 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 6, "name": "name6" }]
};
// Expected result: [{
//   "id": 1,
//   "name": name1,
//   "children": [
//     { "id": 2, "name": "name2" },
//     { "id": 6, "name": "name6" }
//   ]
// }, {
//   "id": 3,
//   "name": "name3",
//   "children": [{"id": 4, "name": "name4" }]
// }]

function mergeEquallyLabeledTypes(collector, type) {
  var key = (type.name + '@' + type.id); // identity key.
  var store = collector.store;
  var storedType = store[key];
  if (storedType) { // merge `children` of identically named types.
    storedType.children = storedType.children.concat(type.children);
  } else {
    store[key] = type;
    collector.list.push(type);
  }
  return collector;
}

var result = [obj1, obj2, obj3].reduce(mergeEquallyLabeledTypes, {

  store:  {},
  list:   []

}).list;

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

Edit Note

After having been informed about changed requirements, that need to deal with a nested pattern, I will change my first provided approach into a generic solution. It will be not that difficult since there is a generically repeated pattern within the data structure. Thus I just need to make the already existing reducer function self recursive. A recursion step will be triggered after having finished a complete reducing cycle on any provided list ...

var obj1 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 2, "name": "name2", "children": [{ "id": 8, "name": "name8" }] }]
};
var obj2 = {
  "id": 3,
  "name": "name3",
  "children": [{ "id": 4, "name": "name4", "children": [{ "id": 9, "name": "name9" }] }]
};
var obj3 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 6, "name": "name6", "children": [{ "id": 10, "name": "name10" }] }]
};
var obj4 = {
  "id": 3,
  "name": "name3",
  "children": [{ "id": 4, "name": "name4", "children": [{ "id": 11, "name": "name11" }] }]
};

function mergeEquallyLabeledTypesRecursively(collector, type, idx, list) {
  var key = (type.name + '@' + type.id); // identity key.
  var store = collector.store;
  var storedType = store[key];
  if (storedType) { // merge `children` of identically named types.
    storedType.children = storedType.children.concat(type.children);
  } else {
    store[key] = type;
    collector.list.push(type);
  }
  // take repetitive data patterns into account ...
  if (idx >= (list.length - 1)) {
    collector.list.forEach(function (type) {

      // ... behave recursive, when appropriate.
      if (type.children) {
        type.children = type.children.reduce(mergeEquallyLabeledTypesRecursively, {

          store:  {},
          list:   []

        }).list;
      }
    });
  }
  return collector;
}

var result = [obj1, obj2, obj3, obj4].reduce(mergeEquallyLabeledTypesRecursively, {

  store:  {},
  list:   []

}).list;

console.log('result : ', result);
.as-console-wrapper { max-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • Wow, I would say this snippet works for me. But this only managed to merge my array at the first level, the parent. It does not work for the nested children. – Ronaldo Oct 31 '17 at 02:27
  • @Ronaldo ... If there was an example that shows/describes the new requirements we might be able to adapt our approaches accordingly. – Peter Seliger Oct 31 '17 at 17:35
  • you can check out this answer I posted.. using ur approach, i've built on it. Is there anyway this can be improved? https://stackoverflow.com/a/47027531/7124504 – Ronaldo Nov 01 '17 at 02:22
2

This might be what your after, please note it's not recursive now recursive. But your example data doesn't appear to be anyway.

const object1 = {"id":1,
           "name":"name1",
           "children":[{"id":2,"name":"name2"}]
          };
const object2 = {"id":3,
           "name":"name3",
           "children":[{"id":4,"name":"name4"}]
          };
const object3 = {"id":1,
           "name":"name1",
           "children":[
              {"id":6,"name":"name6"},
              {"id":7,"name":"name7"},
              {"id":6,"name":"name6"}
           ]
          };
    
function merge(arr) {
  const idLinks = {};
  const ret = [];
  arr.forEach((r) => {
    if (!idLinks[r.id]) idLinks[r.id] = [];
    idLinks[r.id].push(r);
  });
  Object.keys(idLinks).forEach((k) => {
    const nn = idLinks[k];
    const n = nn[0];
    for (let l = 1; l < nn.length; l ++) {
      if (nn[l].children) {
        if (!n.children) n.children = [];
        n.children = n.children.concat(nn[l].children);
      }      
    }    
    if (n.children && n.children.length) n.children = merge(n.children);
    ret.push(n);
  });
  return ret;
}
         
var result = merge([object1,object2,object3]);

console.log(result);
Keith
  • 22,005
  • 2
  • 27
  • 44
  • Just done a quick update to handle recursive ID's, if you look at the second id 1, it has 2 id 6's. These will now also get merged. – Keith Oct 27 '17 at 09:38
2

In functional programming way with es6 standards. I am assuming children array also contains duplicates. I enclosed the code in closures.

See the following link why I used util to print all the object in node console.log()

How can I get the full object in Node.js's console.log(), rather than '[Object]'?

(function() {

'use strict';

const util = require('util');

/** string constants */
const ID = 'id';
const CHILDREN = 'children';

/* Objects to modify */
const object1 = {
    "id": 1,
    "name": "name1",
    "children": [
        { "id": 2, "name": "name2" },
        { "id": 5, "name": "name5" },
        { "id": 7, "name": "name7" }
    ]
};
const object2 = {
    "id": 3,
    "name": "name3",
    "children": [
        { "id": 4, "name": "name4" }
    ]
};
const object3 = {
    "id": 1,
    "name": "name1",
    "children": [
        { "id": 5, "name": "name5" },
        { "id": 6, "name": "name6" }
    ]
};

/**
 * Concates the arrays
 * @param { array }  - a
 * @param { array }  - b
 */
const merge = (a, b) => {
    return a.concat(b);
};

/**
 * Removes Duplicates from the given array based on ID
 * @param  { array } - array to remove duplicates
 * @return { array } - array without duplicates
 */
const removeDuplicates = (arr) => {
    return arr.filter((obj, pos, arr) => {
        return arr.map((m) => {
            return m[ID];
        }).indexOf(obj[ID]) === pos;
    });
}

/**
 * Groups items in array with particular key
 * Currying technique
 * @param  { prop }     - key to group
 * @return { () => {} } - Method which in turn takes array as argument
 */
const groupBy = (prop) => (array) => {
    return array.reduce((groups, item) => {
        const val = item[prop];
        groups[val] = groups[val] || [];
        groups[val].push(item);
        return groups;
    }, {});
}

/**
 * Object containing grouped-items by particuar key
 */
const grouped = groupBy(ID)([object1, object2, object3]);

/**
 * Removing the duplicates of children
 * Remember map also mutates the array of objects key's value
 * but not data type
 */
Object.keys(grouped).map((key, position) => {
    grouped[key].reduce((a, b) => {
        a[CHILDREN] = removeDuplicates(a[CHILDREN].concat(b[CHILDREN]));
    });
});

/**
 * Desired final output
 */
const final = Object.keys(grouped)
    .map((key) => removeDuplicates(grouped[key]))
    .reduce(merge, []);

console.log(util.inspect(final, false, null))})();
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Victor
  • 379
  • 3
  • 13
2
/* There are two cases : 

 a) No duplicate children 
 b) Duplicate children either in (same object || different object|| both) 
*/ 

/* =============== */

/* Case a) */
const util = require('util');
var object1 = {
    "id": 1,
    "name": "name1",
    "children": [{ "id": 2, "name": "name2" }]
};

var object2 = {
    "id": 3,
    "name": "name3",
    "children": [{ "id": 4, "name": "name4" }]
};

var object3 = {
    "id": 1,
    "name":"name1",
    "children":[{"id":6,"name":"name6"}]
};


var arr = [object1,object2,object3];
var uniqueIds = [];
var filteredArray = [];
var uniqueId='';



    arr.map((item,i,array)=>{
    uniqueId =uniqueIds.indexOf(item.id);
    uniqueId = uniqueId+1;
    uniqueIds = [...uniqueIds,item.id];
    if(!uniqueId){
        filteredArray[i] = item;
    }
    if(uniqueId){
        filteredArray[uniqueId-1]['children'] = [...(array[uniqueId-1].children),...(item.children)];
    } 
});

console.log(util.inspect(filteredArray,false,null));

/* ============================================ 
 Case b) 

 Dealing with the worst case of having duplicate children in both same 
 and different objects
*/

    object1 = {"id":1,
    "name":'name1',
    "children":[{"id":2,"name":'name2'},
    {"id":2,"name":'name2'}]
    };
    object2 = {"id":3,
    "name":'name3',
    "children":[{"id":4,"name":'name4'}]
    };
    object3 = {"id":1,
    "name":'name1',
    "children":[{"id":6,"name":'name6'},
    {"id":7,"name":'name7'},
    {"id":2,"name":'name2'}]
    };

    arr = [object1,object2,object3];
    uniqueIds = [];
    uniqueId = '';




arr.map((item,i,array)=>{
    uniqueId =uniqueIds.indexOf(item.id);
    uniqueId = uniqueId+1;
    uniqueIds = [...uniqueIds,item.id];
    if(!uniqueId){
        filteredArray[i] = item;
    }
    if(uniqueId){
        filteredArray[uniqueId-1]['children'] = [...(array[uniqueId-1].children),...(item.children)];
    } 
    /*Removing duplicate children entries*/
    filteredArray[uniqueIds.indexOf(item.id)]['children'] = filteredArray[uniqueIds.indexOf(item.id)]['children']
    .filter((elem, index, self) => self.findIndex((t) => {return t.id === elem.id}) === index)
})

console.log(util.inspect(filteredArray,false,null));
Lava kumar N R
  • 425
  • 6
  • 10
1
const object1 = {
      "id":1,
      "name":"name1",
      "children":[{"id":2,"name":"name2"}]
    };
    const object2 = {
      "id":3,
      "name":"name3",
      "children":[{"id":4,"name":"name4"}]
    };
    const object3 = {
      "id":1,
      "name":"name1",
      "children":[{"id":6,"name":"name6"}]
    };

    var array = [object1,object2,object3];
    var array2 = [object1,object2,object3];

    function uniquearray(obj){
      var result =[];
      for(var i=0;i<array.length;i++){
        if(obj.id == array[i].id){
          result.push(array[i])
          array.splice(i,1)
        }
      }
      return result;
    }

    var arrayofuniarrays = []
    for(var i=0;i<array2.length;i++){
      arrayofuniarrays.push(uniquearray(array2[i]))
    }

    for(var i=0;i<arrayofuniarrays.length;i++){
      for(var j=1;j<arrayofuniarrays[i].length; j++){
        arrayofuniarrays[i][0].children.push(arrayofuniarrays[i][j].children)
        arrayofuniarrays[i].splice(j,1)
      }
    }
    var resul = arrayofuniarrays.reduce(function(a, b){return a.concat(b)},[])
    console.log(resul)
  • 3
    While this code snippet may be the solution, [including an explanation](//meta.stackexchange.com/questions/114762/explaining-entirely-‌​code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – yivi Oct 27 '17 at 14:57
0

Here is a sketch example of how to do this. It leverages a mapped type using your id as a key to ensure each item only appears once. It adds all of the children to an array based on the id.

If you needed to enforce the same behaviour on the children, you could use the same technique.

I have split this into multiple iterations to show you the individual parts in play.

Usually, it is more efficient to avoid creating objects that need to be zipped back up if you can.

    const object1 = {
        "id": 1,
        "name": "name1",
        "children": [{ "id": 2, "name": "name2" }]
    };
    
    const object2 = {
        "id": 3,
        "name": "name3",
        "children": [{ "id": 4, "name": "name4" }]
    };
    
    const object3 = {
        "id": 1,
        "name":"name1",
        "children":[{"id":6,"name":"name6"}]
    };
    
    const all = [object1, object2, object3];
    
    // Use a map like a dictionary to enforce unique keys
    const mapped = {};
    for (let obj of all) {
        if (!mapped[obj.id]) {
            mapped[obj.id] = obj;
            continue;
        }
    
        mapped[obj.id].children.push(obj.children);
    }
    console.log('Mapped ==> '+JSON.stringify(mapped));
    
    // If you want to convert the mapped type to an array
    const result = [];
    for (let key in mapped) {
        result.push(mapped[key]);
    }
    
    console.log('Array ==> '+JSON.stringify(result));
Tushar Walzade
  • 3,737
  • 4
  • 33
  • 56
Fenton
  • 241,084
  • 71
  • 387
  • 401
0

Building on @Peter Seliger's answer here, I derived with the following method to merge arrays with deeply nested children.

Given the following objects:

var obj1 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 2, "name": "name2", children:[{ "id":8, "name": "name8" }]  }]
};
var obj2 = {
  "id": 3,
  "name": "name3",
  "children": [{ "id": 4, "name": "name4", children:[{ "id":9, "name": "name9" }] }]
};
var obj3 = {
  "id": 1,
  "name": "name1",
  "children": [{ "id": 6, "name": "name6", children:[{ "id":10, "name": "name10" }] }]
};
var obj4 = {
  "id": 3,
  "name": "name3",
  "children": [{ "id": 4, "name": "name4", children:[{ "id":11, "name": "name11" }] }]
};    

First we merge the parents

function mergeEquallyLabeledTypes(collector, type) {
  var key = (type.name + '@' + type.id); // identity key.
  var store = collector.store;
  var storedType = store[key];
  if (storedType) { // merge `children` of identically named types.
    if(storedType.children)
       storedType.children = storedType.children.concat(type.children);
  } else {
    store[key] = type;
    collector.list.push(type);
  }
  return collector;
}

var result = [obj1, obj2, obj3, obj4].reduce(mergeEquallyLabeledTypes, {    
  store:  {},
  list:   []    
}).list;

Then we merge the children and subchildren if any.

for(let i=0; i<result.length; i++){
   var children = result[i].children;
   if(children){
     var reducedChildren = children.reduce(mergeEquallyLabeledTypes, {store: {},    list: []}).list;

      for(let j=0; j<reducedChildren.length; j++){
        var subchildren = reducedChildren[j].children;
        if(subchildren){
           var reducedSubchildren = subchildren.reduce(mergeEquallyLabeledTypes, {store: {},    list: []}).list;
            reducedChildren[j].children = reducedSubchildren;
        }                           
      }

     result[i].children = reducedChildren;
   }                    
 }

Finally the result will be what I'll parse into my website.

console.log('result : ', result);

I am able to get the expected result.

    // result: [{
    //   "id": 1,
    //   "name": name1,
    //   "children": [
    //     { "id": 2, "name": "name2", children:[{ "id":8, "name": "name8" }] },
    //     { "id": 6, "name": "name6", children:[{ "id":10, "name": "name10" }] }
    //   ]
    // }, {
    //   "id": 3,
    //   "name": "name3",
    //   "children": [{"id": 4, "name": "name4", children:[
    //                                              { "id":9, "name": "name9" },
    //                                              { "id":11, "name": "name11" }
    //                                            ]  
    //               }
    //    ]
    // }]

However, this might not be too efficient as I'll need to keep adding on to the merging of children/subchildren method if my tree get nested with more levels. (e.g. subsubchildren, subsubsubchildren and so on...)

Is there any more efficient way to do this?

Ronaldo
  • 263
  • 5
  • 18
  • 3
    ok now I get what you were referring to in your comment to my first answer/approach. And as you already do mention yourself, the solution of yours is not generic and also not that efficient. So please stay honest to yourself and don't accept your own answer. I in return will [edit my answer and add a second iteration step to the provided approach](https://stackoverflow.com/questions/46971158/how-to-merge-objects-with-the-same-properties-into-an-array/46972995#46972995) that makes this solution generic to your requirements. – Peter Seliger Nov 01 '17 at 11:23
0
const object1 = {
    id:1,
    name:'a',
}

const object2 = {
    id:3,
    name:'b',
}

const object3 = {
    id:1,
    name:'c',
}

const originArr = [object1, object2, object3]
const idArr = [object1.id, object2.id, object3.id]
const newIdArr = []

for (let id of idArr) {
    if (newIdArr.indexOf(id)) newIdArr.push(id)
}

const result = newIdArr.map(id => {
    let names = []
    for (obj of originArr) {
        if (id === obj.id) names.push(obj.name)
    }

    return { id, names }
})

console.log(result)
  • Welcome to StackOverflow! Note that this significantly changes the input structure, and I have no idea if it will solve the original problem without trying it myself. See [How do I create a runnable stack snippet?](https://meta.stackoverflow.com/questions/358992) to learn how to make this easier to test. – Scott Sauyet May 16 '22 at 12:29