47

Sets are supposed to contain unique objects, but it doesn't work for objects in javascript.

var set = new Set()
<- undefined
set.add({name:'a', value: 'b'})
<- Set {Object {name: "a", value: "b"}}
set.add({name:'a', value: 'b'})
<- Set {Object {name: "a", value: "b"}, Object {name: "a", value: "b"}}

It works for primitives

var b = new Set()
<- undefined
b.add(1)
<- Set {1}
b.add(2)
<- Set {1, 2}
b.add(1)
<- Set {1, 2}

So how do I get it to work with objects? I get the fact that they are different objects with the same values, but I'm looking for like a deep unique set.

EDIT:

Here's what I'm actually doing

    var m = await(M.find({c: cID}).populate('p')) //database call
    var p = new Set();
    m.forEach(function(sm){
        p.add(sm.p)
    })

This is to get a unique list of sm.p

Matt Westlake
  • 3,499
  • 7
  • 39
  • 80
  • 8
    *"I get the fact that they are different objects with the same values"* - Exactly. They are *different* objects. And even if they originally had unique property values, after being added to the set they could be modified to have the same property values. – nnnnnn Dec 31 '16 at 01:21
  • @nnnnnn So how do I achieve what I'm looking for which is a Set of objects with unique values? (i would also accept an array of unique object values) – Matt Westlake Dec 31 '16 at 01:26
  • 1
    This [question](http://stackoverflow.com/questions/28991014/underscore-js-remove-duplicates-in-array-of-objects-based-on-key-value) might help. – Michael Sacket Dec 31 '16 at 01:27
  • @MattWestlake, most likely you don't want to use objects as keys for your set, but a JSON serialization with sorted keys. – Kijewski Dec 31 '16 at 01:27
  • Can you guarantee that the objects won't be modified after they're added to the set? – nnnnnn Dec 31 '16 at 01:28
  • @nnn given the nature of objects, I don't think that is possible. –  Dec 31 '16 at 01:30
  • @MichaelSacket How would I use that with multiple properties? (EX: I could have `{name: 'a', value: 'b'}` and `{name: 'a', value: 'c'}` If I do unique on just name, I'll still loose a value – Matt Westlake Dec 31 '16 at 01:33
  • @TinyGiant - Within the OP's code he might never modify them once created. Or could `Object.freeze()` them... – nnnnnn Dec 31 '16 at 01:34
  • @Kay I thought about doing that but I would need to do a JSON.stringify on every add to the set. Is that really what I want to do? – Matt Westlake Dec 31 '16 at 01:35
  • 3
    If you're doing a database operation can't you have the database query return unique results in the first place? Databases are good at that sort of thing. – nnnnnn Dec 31 '16 at 01:46
  • @nnnnnn Just tried it and I can't populate the data and keep the messages around (see https://docs.mongodb.com/manual/reference/method/db.collection.distinct/) It just returns the `sm.p` unique data(IDs only) which means I have to do a 2nd query with IN clause containing the result. It looses the `m` result as well. The more I look at it, the more I'm thinking JSON.stringify is what I want. I don't like the performance hit, but it's better then going back to the database multiple times. – Matt Westlake Dec 31 '16 at 02:11
  • if key order seems consistent, just use JSON or a custom deep comparator (for speed) – dandavis Dec 31 '16 at 03:38

8 Answers8

18

Another option is you could use JSON.stringify() to keep your object unique, that way it is comparing against a string instead of an object reference.

set.add(JSON.stringify({name:'a', value: 'b'}))

and then after everything is formatted you can just parse those lines back to an array like this:

const formattedSet = [...set].map(item) => {
  if (typeof item === 'string') return JSON.parse(item);
  else if (typeof item === 'object') return item;
});
Dhia Djobbi
  • 1,176
  • 2
  • 15
  • 35
Cauliflower
  • 446
  • 4
  • 12
  • 2
    A solution like this will need to take order into account. JSON.stringify() cannot be trusted to give results with keys in the same order every time. Besides that there are many values which it does not stringify correctly, such as Infinity, NaN, and recursive objects. – dankuck May 18 '21 at 01:31
  • valid point for stringifying certain kinds of values. Symbol() and first class functions will be removed as well. order also can get knocked out of line in some rare instances, if thats important to you you can do something like `set.add(JSON.stringify(obj, Object.keys(obj).sort())) ` to preserve the order of those top level keys. see https://stackoverflow.com/a/64565203/6621163 – Cauliflower May 21 '21 at 16:31
15

well, if you are looking for a deep unique set, you can make a deep set by youself, by extending the original 'Set', like this:

function DeepSet() {
    //
}
DeepSet.prototype = Object.create(Set.prototype);
DeepSet.prototype.constructor = DeepSet;
DeepSet.prototype.add = function(o) {
    for (let i of this)
        if (deepCompare(o, i))
            throw "Already existed";
    Set.prototype.add.call(this, o);
};
Yichong
  • 707
  • 4
  • 10
  • 6
    This is a great way to add this behavior. Unfortunately there is no native `deepCompare` method. – Scott Dec 31 '16 at 01:59
  • 2
    of course. `DeepSet` has to be implemented by ourselves, just like we have to implement the `deepCompare` method ourselves. – Yichong Dec 31 '16 at 04:47
  • 4
    This is a really bad idea. You would also need to override has, delete, and add. Also, this will run in Omega(n) now. – idmean Mar 08 '22 at 15:32
10

Based on Joe Yichong post, here a suggestion to extend Set for TypeScript.

export class DeepSet extends Set {

  add (o: any) {
    for (let i of this)
      if (this.deepCompare(o, i))
        return this;
    super.add.call(this, o);
    return this;
  };

  private deepCompare(o: any, i: any) {
    return JSON.stringify(o) === JSON.stringify(i)
  }
}

felix_teg
  • 234
  • 3
  • 4
  • 7
    That's missing the point of using a set though: the O(1) add operations. Instead this has to compare each new entry to each previous entry. – Migwell Feb 07 '22 at 14:04
  • 1
    I believe the order of the properties in the string that `JSON.stringify()` returns is not guaranteed. Not sure if comparing these two strings would do the trick. – perepm Oct 01 '22 at 18:33
4

You can use Maps. If you need unique values based on the key. For me I wanted to have unique selection from array of object

let cats = [
    { category: 6 },
    { brand: 'purina' },
    { category: 5 },
    { category: 5 },
    { brand: 'purina' }];

var myMap = new Map();

cats.forEach(object => {
    for (const key in object) {
      myMap.set(key, object[key])
    }
})
console.log(myMap) //Map { 'category' => 5, 'brand' => 'purina' }

Here category and brand isn't repeated

Samiullah Khan
  • 826
  • 10
  • 11
2

Well, Javascript Set don't store unique object because every object will have their own references. Thus, Set will store the unique values only so it treats even two or more object have the same values their references are different.

Example -

var set = new Set();

set.add({ id: 1 });
set.add({ id: 2 });
set.add({ id: 3 });

// Every object has different reference
var d = { id: 4 };

set.add(d); 
set.add(d);

for (const item of set) {
    console.log(item);
}

// Output : 1 2 3 4

Note : The solution is you can create a reference for the same object and add into Set as above example I have done.

anandchaugule
  • 901
  • 12
  • 20
1

A common scenario is when we're getting some data from API, which repeats some of the events.

Being in a for loop we need to use JSON.stringify({...}) when using push(...) on the sampleArray:

for (const id in response.data) {
  sampleArray.push(JSON.stringify({id, ...});
}

Then we can create a Set from the sampleArray:

const sampleSet = new Set(sampleArray);

Now, convert back to an Array:

const list = Array.from(sampleSet);

For each of the elements JSON.parse(...) to have it in the right format again (note: I badly needed the same sampleArray, you can use a new variable):

sampleArray = [];
list.forEach((element) => {
  JSON.parse(element);
  sampleArray.push(JSON.parse(element));
});

Full code:

for (const id in response.data) {
  sampleArray.push(JSON.stringify({id, ...});
}

const sampleSet = new Set(sampleArray)
const list = Array.from(sampleSet);

sampleArray = [];
list.forEach((element) => {
  JSON.parse(element)
  sampleArray.push(JSON.parse(element))
});

Will work fine for that scenario, because we've control over the order, which is crucial, as noted by @dankuck.

Daniel Danielecki
  • 8,508
  • 6
  • 68
  • 94
0

If you can afford the “expense” of using a library, in this case fast-deep-equal (it’s a dependency on thousands of other packages—many staple ones—so it’s likely already in your graph) then there’s a dead simple extension to Set: UniqueSet

It’s basically the same as Joe Yichong’s answer except it’s exported as a module and implements fast-deep-equal to compare each element deeply, so it can be an easy and convenient approach. You could also just write it yourself—it does nothing special nor opaque.

Using fast-deep-equal is more reliable than JSON serialization due to issues with certain data types and sorting, some of which have been mentioned here already.

sepiariver
  • 511
  • 3
  • 8
-6

This is naive and I'm sure there are better ways of getting unique data (like making the database query return unique data, but that would depend on the database), but you can check to see if the object is already in the set before inserting it.

var m = await(M.find({c: cID}).populate('p')) //database call
var p = new Set();
m.forEach(function(sm){
    if(!p.has(sm.p)) p.add(sm.p)
})
  • 12
    Kind of defeats the point of using a set, may as well just use an array. – Ben Jun 05 '18 at 01:14
  • 6
    This comment should have -1. No point in using Set if you're going to check if it exists at 0(n). Plus this is not "naive", I have a React App in which using a data structure like Set (if we could implement it the correct way like in Java or C#) would be my best option. – GabrielBB Jun 27 '18 at 20:46