68

What is the best way to merge array contents from JavaScript objects sharing a key in common?

How can array in the example below be reorganized into output? Here, all value keys (whether an array or not) are merged into all objects sharing the same name key.

var array = [
    {
        name: "foo1",
        value: "val1"
    }, {
        name: "foo1",
        value: [
            "val2",
            "val3"
        ]
    }, {
        name: "foo2",
        value: "val4"
    }
];

var output = [
    {
        name: "foo1",
        value: [
            "val1",
            "val2",
            "val3"
        ]
    }, {
        name: "foo2",
        value: [
            "val4"
        ]
    }
];
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
zdebruine
  • 3,687
  • 6
  • 31
  • 50
  • 1
    is `array` `{name:"foo1",value:"val1"}` going to be `{name:"foo1",value:["val1"]}`? – BenG Nov 22 '15 at 00:57
  • @BG101 no it's not an array but if that helps I can make it beforehand – zdebruine Nov 22 '15 at 01:00
  • 1
    Have you tried using jQuery's extend() method ? Or are you trying to write your own merging script ? So something like this maybe: `var newArray = $.extend({}, array, output);` –  Nov 22 '15 at 01:09
  • 1
    Is there a specific way you're trying to merge the content, or would `$.extend()` suffice ? –  Nov 22 '15 at 01:13
  • @ZachPerkitny I'm not sure $.extend() would suffice because I do have to deal with arrays within the key values. – zdebruine Nov 22 '15 at 01:18
  • Give me an example of how you want it to be merged –  Nov 22 '15 at 01:21

15 Answers15

75

Here is one option:-

var array = [{
  name: "foo1",
  value: "val1"
}, {
  name: "foo1",
  value: ["val2", "val3"]
}, {
  name: "foo2",
  value: "val4"
}];

var output = [];

array.forEach(function(item) {
  var existing = output.filter(function(v, i) {
    return v.name == item.name;
  });
  if (existing.length) {
    var existingIndex = output.indexOf(existing[0]);
    output[existingIndex].value = output[existingIndex].value.concat(item.value);
  } else {
    if (typeof item.value == 'string')
      item.value = [item.value];
    output.push(item);
  }
});

console.dir(output);
BenG
  • 14,826
  • 5
  • 45
  • 60
  • 3
    My `key` and `value` name are different. and I am getting confused because here the `value` name in input object is `value` and the parameter name you have taken in the `forEach` loop is also named value. Please make my confusion clear. Thanks. – Arpit Kumar Mar 03 '17 at 10:28
  • Sorry @ArpitMeena, only now seen your request in the comments. I've changed it to `item` to help in future. – BenG Oct 25 '18 at 07:48
  • what if i have an array with multiple objects all have share one same key , but this key is unknow, for example one of the objects structure is `{ "127.0.0.1" : 1, "127.0.0.2" : 1, "127.0.0.3" : 1, "127.0.0.4" : 1, }` and the second object `{ "127.0.0.1" : 1000, "127.0.0.2" : 1000, "127.0.0.3" : 1000, "127.0.0.4" : 1000, }`, how to merge them so the output, be something lke this `{"127.0.0.1": 1, "my-custom-key": 1000}, second obj etc....`. **ps** the values could be different i just made a simple example. – sasha romanov Aug 15 '19 at 22:37
20

Here is another way of achieving that goal:

var array = [{
  name: "foo1",
  value: "val1"
}, {
  name: "foo1",
  value: [
    "val2",
    "val3"
  ]
}, {
  name: "foo2",
  value: "val4"
}];

var output = array.reduce(function(o, cur) {

  // Get the index of the key-value pair.
  var occurs = o.reduce(function(n, item, i) {
    return (item.name === cur.name) ? i : n;
  }, -1);

  // If the name is found,
  if (occurs >= 0) {

    // append the current value to its list of values.
    o[occurs].value = o[occurs].value.concat(cur.value);

    // Otherwise,
  } else {

    // add the current item to o (but make sure the value is an array).
    var obj = {
      name: cur.name,
      value: [cur.value]
    };
    o = o.concat([obj]);
  }

  return o;
}, []);

console.log(output);
Avraham
  • 916
  • 1
  • 13
  • 25
Hunan Rostomyan
  • 2,176
  • 2
  • 22
  • 31
13

2021 version

var arrays = [{ name: "foo1",value: "val1" }, {name: "foo1", value: ["val2", "val3"] }, {name: "foo2",value: "val4"}];

const result = arrays.reduce((acc, {name, value}) => {
  acc[name] ??= {name: name, value: []};
  if(Array.isArray(value)) // if it's array type then concat 
    acc[name].value = acc[name].value.concat(value);
  else
    acc[name].value.push(value);
  
  return acc;
}, {});

console.log(Object.values(result));
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
11

Using lodash

var array = [{name:"foo1",value:"val1"},{name:"foo1",value:["val2","val3"]},{name:"foo2",value:"val4"}];

function mergeNames (arr) {
    return _.chain(arr).groupBy('name').mapValues(function (v) {
        return _.chain(v).pluck('value').flattenDeep();
    }).value();
}

console.log(mergeNames(array));
paolobueno
  • 1,678
  • 10
  • 9
  • 3
    simplified version and also pluck now should be map : `const mergeNames = arr => _.chain(arr).groupBy('name').mapValues(v => _.chain(v).map('value').flattenDeep()).value(); console.log(mergeNames(array));` – Softmixt Apr 12 '19 at 08:04
  • The data is coming in '2021-04-14': LodashWrapper { __wrapped__: LazyWrapper { __wrapped__: [LodashWrapper], __actions__: [Array], __dir__: 1, __filtered__: false, __iteratees__: [Array], __takeCount__: 4294967295, __views__: [] }, __actions__: [ [Object] ], __chain__: true, __index__: 0, __values__: undefined } this format. how to transform it in array format ? – Swapnil Sudhir Apr 14 '21 at 12:26
9

Here is a version using an ES6 Map:

const arrays = [{ name: "foo1",value: "val1" }, {name: "foo1", value: ["val2", "val3"] }, {name: "foo2",value: "val4"}];

const map = new Map(arrays.map(({name, value}) => [name, { name, value: [] }])); 
for (let {name, value} of arrays) map.get(name).value.push(...[value].flat());
console.log([...map.values()]);
trincot
  • 317,000
  • 35
  • 244
  • 286
4

Using reduce:

var mergedObj = array.reduce((acc, obj) => {
    if (acc[obj.name]) {
       acc[obj.name].value = acc[obj.name].value.isArray ? 
       acc[obj.name].value.concat(obj.value) : 
       [acc[obj.name].value].concat(obj.value);

    } else {
      acc[obj.name] = obj;
    }

    return acc;
}, {});

let output = [];
for (let prop in mergedObj) {
  output.push(mergedObj[prop]) 
}
unamaga
  • 53
  • 3
4

It's been a while since this question was asked, but I thought I'd chime in as well. For functions like this that execute a basic function you'll want to use over and over, I prefer to avoid longer-written functions and loops if I can help it and develop the function as a one-liner using shallow Array.prototype functions like .map() and some other ES6+ goodies like Object.entries() and Object.fromEntries(). Combining all these, we can execute a function like this relatively easily.

First, I take in however many objects you pass to the function as a rest parameter and prepend that with an empty object we'll use to collect all the keys and values.

[{}, ...objs]

Next, I use the .map() Array prototype function paired with Object.entries() to loop through all the entries of each object, and any sub-array elements each contains and then either set the empty object's key to that value if it has not yet been declared, or I push the new values to the object key if it has been declared.

[{},...objs].map((e,i,a) => i ? Object.entries(e).map(f => (a[0][f[0]] ? a[0][f[0]].push(...([f[1]].flat())) : (a[0][f[0]] = [f[1]].flat()))) : e)[0]

Finally, to replace any single-element-arrays with their contained value, I run another .map() function on the result array using both Object.entries() and Object.fromEntries(), similar to how we did before.

let getMergedObjs = (...objs) => Object.fromEntries(Object.entries([{},...objs].map((e,i,a) => i ? Object.entries(e).map(f => (a[0][f[0]] ? a[0][f[0]].push(...([f[1]].flat())) : (a[0][f[0]] = [f[1]].flat()))) : e)[0]).map(e => e.map((f,i) => i ? (f.length > 1 ? f : f[0]) : f)));

This will leave you with the final merged object, exactly as you prescribed it.

let a = {
  a: [1,9],
  b: 1,
  c: 1
}
let b = {
  a: 2,
  b: 2
}
let c = {
  b: 3,
  c: 3,
  d: 5
}

let getMergedObjs = (...objs) => Object.fromEntries(Object.entries([{},...objs].map((e,i,a) => i ? Object.entries(e).map(f => (a[0][f[0]] ? a[0][f[0]].push(...([f[1]].flat())) : (a[0][f[0]] = [f[1]].flat()))) : e)[0]).map(e => e.map((f,i) => i ? (f.length > 1 ? f : f[0]) : f)));

getMergedObjs(a,b,c); // { a: [ 1, 9, 2 ], b: [ 1, 2, 3 ], c: [ 1, 3 ], d: 5 }
Brandon McConnell
  • 5,776
  • 1
  • 20
  • 36
4

Use lodash "uniqWith". As shown below

let _ = require("lodash");

var array = [
  { name: "foo1", value: "1" },
  { name: "foo1", value: "2" },
  { name: "foo2", value: "3" },
  { name: "foo1", value: "4" }
];

let merged = _.uniqWith(array, (pre, cur) => {
  if (pre.name == cur.name) {
    cur.value = cur.value + "," + pre.value;
    return true;
  }
  return false;
});

console.log(merged);
// output:  [{ name: "foo1", value: "1,2,4" }, { name: "foo2", value: "3" }];


JoeH
  • 368
  • 1
  • 3
  • 9
  • how would I go about doing exactly this, but get the sum of foo1 values to be 7? (1+2+4) – Aaron Jun 19 '22 at 19:52
3

Try this:

var array = [{name:"foo1",value:"val1"},{name:"foo1",value:["val2","val3"]},{name:"foo2",value:"val4"},{name:"foo2",value:"val5"}];

for(var j=0;j<array.length;j++){
  var current = array[j];
  for(var i=j+1;i<array.length;i++){
    if(current.name = array[i].name){
      if(!isArray(current.value))
        current.value = [ current.value ];
      if(isArray(array[i].value))
         for(var v=0;v<array[i].value.length;v++)
           current.value.push(array[i].value[v]);
      else
        current.value.push(array[i].value);
      array.splice(i,1);
      i++;
    }
  }
}

function isArray(myArray) {
    return myArray.constructor.toString().indexOf("Array") > -1;
}

document.write(JSON.stringify(array));
Racil Hilan
  • 24,690
  • 13
  • 50
  • 55
  • this is foolproof, but it's the traditional brute force method that I'm trying to get around. – zdebruine Nov 22 '15 at 02:41
  • 1
    Yeah, but I did not use "endless" loops :-), I only used two loops and more importantly, I worked directly on the `array` variable eliminating the need for another `output` variable which makes it more memory efficient. However, I also liked the two answers from BG101 and Hunan, so I upvoted them. – Racil Hilan Nov 22 '15 at 14:26
3

This work too !

      var array = [
        {
          name: "foo1",
          value: "val1",
        },
        {
          name: "foo1",
          value: ["val2", "val3"],
        },
        {
          name: "foo2",
          value: "val4",
        },
      ];
      let arr2 = [];
      array.forEach((element) => { // remove duplicate name
        let match = arr2.find((r) => r.name == element.name);
        if (match) {
        } else {
          arr2.push({ name: element.name, value: [] });
        }
      });
      arr2.map((item) => {
        array.map((e) => {
          if (e.name == item.name) {
            if (typeof e.value == "object") { //lets map if value is an object
              e.value.map((z) => {
                item.value.push(z);
              });
            } else {
              item.value.push(e.value);
            }
          }
        });
      });
      console.log(arr2);
qahtan said
  • 49
  • 1
  • 3
  • How could you Add the values of foo1? Basically add `val1, val2, and val3` into 1 value? – Mars2024 Dec 01 '21 at 21:06
  • i'm using temp arr2 to save the value, then mapping them again to see is an object, then i create an array on it, if not an object/array i push the value to it @jedLancer – qahtan said Dec 03 '21 at 06:12
2
const exampleObj = [{
  year: 2016,
  abd: 123
}, {
  year: 2016,
  abdc: 123
}, {
  year: 2017,
  abdcxc: 123
}, {
  year: 2017,
  abdcxcx: 123
}];
const listOfYears = [];
const finalObj = [];
exampleObj.map(sample => {    
  listOfYears.push(sample.year);
});
const uniqueList = [...new Set(listOfYears)];
uniqueList.map(list => {   
  finalObj.push({
    year: list
  });
});
exampleObj.map(sample => {    
  const sampleYear = sample.year;  
  finalObj.map((obj, index) => {     
    if (obj.year === sampleYear) {        
      finalObj[index] = Object.assign(sample, obj);       
    }  
  }); 
});

The final object be [{"year":2016,"abdc":123,"abd":123},{"year":2017,"abdcxcx":123,"abdcxc":123}]

2

const array = [{ name: "foo1", value: "val1" }, {name: "foo1", value: ["val2", "val3"] }, {name: "foo2", value: "val4"}];
const start = array.reduce((object, {name}) => ({...object, [name]: []}), {});
const result = array.reduce((object, {name, value}) => ({...object, [name]: [object[name], [value]].flat(2)}), start);
const output = Object.entries(result).map(([name, value]) => ({name: name, value: value}));
console.log(output);
Gil
  • 21
  • 4
1

try this :

    var array = [
      {
          name: "foo1",
          value: "val1"
      }, {
          name: "foo1",
          value: [
              "val2",
              "val3"
          ]
      }, {
          name: "foo2",
          value: "val4"
      }
  ];
  
  var output = [
      {
          name: "foo1",
          value: [
              "val1",
              "val2",
              "val3"
          ]
      }, {
          name: "foo2",
          value: [
              "val4"
          ]
      }
  ];

  bb = Object.assign( {}, array, output );

console.log(bb) ; 
LookWorld
  • 31
  • 2
1

A much more easier approach is this 2022:

  var array = [
        {
            name: "foo1",
            value: "val1"
        }, {
            name: "foo1",
            value: [
                "val2",
                "val3"
            ]
        }, {
            name: "foo2",
            value: "val4"
        }
    ];
    
    var output = [
        {
            name: "foo1",
            value: [
                "val1",
                "val2",
                "val3"
            ]
        },
        {
            name: "foo2",
            value: [
                "val4"
            ]
        }
    ];
    
    function mergeBasedOnKey(list){
      let c = Object.values(list.reduce((a, b) => {
    
        a[b.name] = a[b.name] || {name: b.name, value: []}
        if(typeof(b['value']) == "string"){
          a[b.name].value.push(b['value'])
        }
        else{
          a[b.name].value = [...a[b.name].value, ...b.value]
        }
    
        return a
    
      }, {}))
      return c
    }
   let ans = mergeBasedOnKey(array)
   console.log(ans)
optimus
  • 140
  • 8
  • you are a life saver! very cool way to achieve this! Literally I tried all answers those are good. but this is awesome thanks! – DineshMsd Jan 06 '23 at 17:56
1

I was looking for a quick, almost "one-liner" answer in this thread, provided that this is a trivial but common exercise.

I couldn't find any for my like. The other answers are fine but I am not much into boilerplate.

So, let me add one, then:

o = array.reduce((m,{name:n,value:v})=>({...m,[n]:[...m[n]||[],v].flat(1)}),{})
output = Object.entries(o).map(([n,v])=>({name:n,value:v}))

var array = [
  { name: "foo1", value: "val1"}, 
  { name: "foo1", value: ["val2","val3"] }, 
  { name: "foo2", value: "val4" }
]

o=array.reduce((m,{name:n,value:v})=>({...m,[n]:[...m[n]||[],v].flat(1)}),{})
output=Object.entries(o).map(([n,v])=>({name:n,value:v}))
    
console.log(output)
Rodrigo Rodrigues
  • 7,545
  • 1
  • 24
  • 36