6

Suppose I have following arrays of objects

var firstDataSet = [
  {'id': 123, 'name': 'ABC'},
  {'id': 456, 'name': 'DEF'},
  {'id': 789, 'name': 'GHI'},
  {'id': 101, 'name': 'JKL'}
];

var secondDataSet = [
  {'id': 123, 'name': 'ABC', 'xProp': '1q'},
  {'id': 156, 'name': 'MNO', 'xProp': '2w'},
  {'id': 789, 'name': 'GHI', 'xProp': '3e'},
  {'id': 111, 'name': 'PQR', 'xProp': '4r'}
];

Now I want to collect array with unique objects (matching id and name)i.e.

var firstDataSet = [
  {'id': 123, 'name': 'ABC', 'xProp': '1q'},
  {'id': 456, 'name': 'DEF'},
  {'id': 789, 'name': 'GHI', 'xProp': '3e'},
  {'id': 101, 'name': 'JKL'},
  {'id': 156, 'name': 'MNO', 'xProp': '2w'},
  {'id': 111, 'name': 'PQR', 'xProp': '4r'}
];

I am able to collect ALL with

Array.prototype.unshift.apply(firstDataSet , secondDataSet );

But not sure how I can filter out duplicates. Any suggestion?

Edit: My object on two different array are not same. At least based on number of properties.

αƞjiβ
  • 3,056
  • 14
  • 58
  • 95

4 Answers4

10

To Remove Duplicates With All Identical Properties

This was the original question.

Use a Set:

The Set object lets you store unique values of any type, whether primitive values or object references.

You can also use object literals.

var list = [JSON.stringify({id: 123, 'name': 'ABC'}), JSON.stringify({id: 123, 'name': 'ABC'})]; 
var unique_list = new Set(list); // returns Set {"{'id': 123, 'name': 'ABC'}"}
var list = Array.from(unique_list); // converts back to an array, and you can unstringify the results accordingly.

For more ways to construct a set back to an array, you can follow instructions here. If you can't use ES6 (which is what defines Set), there's a polyfill for older browsers.


To Remove Objects with Duplicate Subset of Properties

Unfortunately, these objects are no longer strictly duplicates and cannot be tackled in a friendly way using Set, for instance.

The easiest way to approach this type of problem is to iterate through the array of objects, identify those with repeated property values, and eliminate in place using splice, for example.

Akshat Mahajan
  • 9,543
  • 4
  • 35
  • 44
  • what if he cant use es6? – omarjmh Mar 31 '16 at 22:46
  • Updated my answer to point out this only works in ES6. A lot of browsers have pretty much introduced support for ES6. There's a [polyfill](https://github.com/WebReflection/es6-collections) for `Set`. – Akshat Mahajan Mar 31 '16 at 22:49
  • If you use `firstDataSet.concat(secondDataSet)` this will create a new array without unsetting the original arrays, so you may also want `firstDataSet = secondDataSet = null` – frajk Mar 31 '16 at 22:51
  • Sorry all for not being clear on first place. My object on second array have some extra properties that's why I can't use Set. – αƞjiβ Mar 31 '16 at 22:51
  • @αƞjiβ If they have extra properties, they're not - strictly speaking - duplicates. The best you can do is iterate through the new array, find those with matching properties, and eliminate. – Akshat Mahajan Mar 31 '16 at 22:53
  • I upvoted because of all the work you did for this question – omarjmh Mar 31 '16 at 23:01
  • Using the string value of an object as the Map key will not work very well. Most basically, it will not consider the keys the same if the properties are listed in a different order. –  Apr 01 '16 at 02:50
  • @frajk Why should I worry about unsetting the original arrays? –  Apr 01 '16 at 03:12
  • @torazaburo free up memory, assuming the original arrays are no longer needed, as I assumed by op's original method to combine the arrays – frajk Apr 01 '16 at 04:05
  • @frajk There is no need to free up memory. JS garbage collects after itself without any help. This is not C where you are doing mallocs and frees. –  Apr 01 '16 at 04:18
  • @torazaburo assuming the original arrays are global in scope, would js still garbage collect? – frajk Apr 01 '16 at 04:40
  • @frajk Most likely not. So if you had a million such global objects, you might have a problem and want to force GC. –  Apr 01 '16 at 04:44
  • @AkshatMahajan I don't quite understand how your response addresses the question, since you converted the parameters to an array of strings. The original questions uses an array of objects. See counter-example: https://repl.it/@makstaks/unique-objects-in-set – makstaks Dec 24 '20 at 01:00
  • @makstaks You are correct, but I'm not following why casting objects to object literals with e.g. `JSON.stringify` before passing to `Set` does not answer the question. There is no prohibition against such casts. I will update my answer to include casting is necessary. – Akshat Mahajan Dec 24 '20 at 15:36
  • You can also do: return Array.from(new Set(arr.map(item => JSON.stringify(item)))).map(item => JSON.parse(item)); – Guilherme do Valle Jul 29 '22 at 19:15
1

This can be achieved By extending Set class Like below

    var firstDataSet = [
      {'id': 123, 'name': 'ABC'},
      {'id': 456, 'name': 'DEF'},
      {'id': 789, 'name': 'GHI'},
      {'id': 101, 'name': 'JKL'}
    ];

    var secondDataSet = [
      {'id': 123, 'name': 'ABC', 'xProp': '1q'},
      {'id': 156, 'name': 'MNO', 'xProp': '2w'},
      {'id': 789, 'name': 'GHI', 'xProp': '3e'},
      {'id': 111, 'name': 'PQR', 'xProp': '4r'}
    ];

    Array.prototype.unshift.apply(firstDataSet , secondDataSet );

    //console.log(firstDataSet)

    class UniqueSet extends Set {
            constructor(values) {
                super(values);

                const data = [];
                for (let value of this) {
                    if (data.includes(JSON.parse(value.id))) {
                        this.delete(value);
                    } else {
                        data.push(value.id);
                    }
                }
            }
          }

console.log(new UniqueSet(firstDataSet))

Working link

Shubham Dixit
  • 9,242
  • 4
  • 27
  • 46
0

This may not be the most efficient solution, but assuming that id is always unique, it should work.

var firstDataSet = [
  {'id': 123, 'name': 'ABC'},
  {'id': 456, 'name': 'DEF'},
  {'id': 789, 'name': 'GHI'},
  {'id': 101, 'name': 'JKL'}
];

var secondDataSet = [
  {'id': 123, 'name': 'ABC', 'xProp': '1q'},
  {'id': 156, 'name': 'MNO', 'xProp': '2w'},
  {'id': 789, 'name': 'GHI', 'xProp': '3e'},
  {'id': 111, 'name': 'PQR', 'xProp': '4r'}
];

Array.prototype.unique = function() {
    var o = {}, i, l = this.length, r = [];
    for(i=0; i<l;i+=1) o[this[i]] = this[i];
    for(i in o) r.push(o[i]);
    return r;
};

function concatUnique(a, b, property) {
    var arr = a.concat(b);
    arr.sort(function(a,b) { 
        return Object.keys(b).length - Object.keys(a).length; 
    });
    var ids = arr.map(function(obj){ return obj[property] }).unique();

    return arr.filter(function(obj) { 
        if(ids.indexOf(obj[property]) > -1) { 
            ids.splice( ids.indexOf(obj[property]) , 1); 
            return true; 
        } else { 
            return false 
        }
    });
}

var newArray = concatUnique(firstDataSet, secondDataSet, 'id');
frajk
  • 853
  • 6
  • 14
  • This does not seem to add the `xProp: '1q'` property from the `123` entry in the second dataset to the corresponding entry in the first dataset as the OP wants. –  Apr 01 '16 at 05:00
  • @torazaburo oh gee, you're right. fixed. kind of. this answer will work for OP's example data by simply keeping the duplicate object with the larger number of properties, but if there were duplicate objects with an equal number but different properties, for example, it would not work. – frajk Apr 01 '16 at 05:20
0

We'll combine the two arrays using concat, then filter the resulting array using filter. For each element, we'll find the index of the first element with the same id and name, using findIndex. If that index is the same as the current index, it means this is the first occurrence of that id and name, so we just let it pass through. Otherwise, we'll add in new fields to the first occurrence, and filter it out.

function combine(a1, a2) {

  function match(e1, e2) { return e1.id === e2.id && e1.name === e2.name); }

  return a1.concat(a2) . filter((e1, i, a) => {
    let firstIndex = a.findIndex(e2 => match(e1, e2));
    if (i === firstIndex) return true; // this is the first occurrence
    a[firstIndex].xProp = e2.xProp;    // copy over property
    return false;                      // filter out
  });

}

If you want to handle arbitrary properties, instead of just xProp, then change the relevant line to something like

a[firstIndex] = Object.assign(e2, a[firstIndex]);

That will replace the first occurrence with the result of copying all its properties on top of the current occurrence including whatever additional properties it may have.

Mandatory disclaimer: As always, depending on your environment, you may not have arrow functions, or Array#findIndex, or Object.assign. In such cases, rewrite/polyfill/transpile as necessary.

  • id be interested in your view on the difference between my method and yours, ignoring the obvious difference that yours is checking two properties and mine just the one. But if both of our methods were checking just one property or both, what's the performance or other notable differences between them? – frajk Apr 01 '16 at 04:54