Related
I have an object with some properties such as;
integrationConfig = {
iconEmoji: ':myIconEmoji:',
team: 'myTeam',
text: 'myText',
channel: 'myChannel',
botName: 'myBot'
}
I am passing this object to a function below as shown (attachments is not important).
return await this.pushToSlack(...integrationConfig, attachments);
Importantly, this function is part of an NPM Package, so I don’t want to change the function declaration.
The function is declared like this:
exports.pushToSlack = function (channel, text, botName, iconEmoji, team, attachments, cb = function () {}) {
// […]
}
I put some breakpoint to the pushToSlack function but the debugger didn’t jump into that line. I guess the function is not called somehow. I also receive this error:
Debug: internal, implementation, error
TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
at Function.all (<anonymous>)
Have you got any idea?
If you cannot change the parameter list of the function, you’ll have to define the order the arguments are expected in, then map your object onto this order:
const argumentOrder = [
"channel",
"text",
"botName",
"iconEmoji",
"team"
];
// […]
return await this.pushToSlack(...argumentOrder.map((property) => integrationConfig[property]), attachments);
The error you’re getting means that func(...integrationConfig) won’t work. Yes, the function is never called. There’s a distinction between object spread and iterable spread. Arguments and arrays use iterable spread, which means that two conditions must be met: firstly, the value you want to spread must be non-nullish; and secondly, the value must be iterable, i.e. something that has Symbol.iterator. Object spread only checks the first condition.
You could, theoretically, add such a symbol property into your object, which would allow you to use your original syntax:
const integrationConfig = {
iconEmoji: ":myIconEmoji:",
team: "myTeam",
text: "myText",
channel: "myChannel",
botName: "myBot",
*[Symbol.iterator](){
yield this.channel;
yield this.text;
yield this.botName;
yield this.iconEmoji;
yield this.team;
}
};
// […]
return await this.pushToSlack(...integrationConfig, attachments);
so you have function
const pushToSlack = function (channel, text, botName, iconEmoji, team, attachments, cb = function () {}) {
this function accepts 7 arguments.
You have good idea to spread an object integrationConfig, hoping that it will become 7 arguments.
However spreading an object will just clone the exact same object with exact same property.
const obj = {...integrationConfig} is equal to ONE Object.
You can instead pass an array and spread it.
pushToSlack(...Object.values(integrationConfig))
But an object does not guarantee the order of the key. so it could be
// order is not guaranteed!!!
...Object.values(integrationConfig) === [botName, channel, iconEmoji, team, text]
// or
...Object.values(integrationConfig) === [channel, botName, iconEmoji, team, text]
// or
...Object.values(integrationConfig) === [team, botName, iconEmoji, channel, text]
However your function need a fix order of arguments.
channel as first argument,
text as second argument
etc.
I believe you can do like this
const integrationConfig = {
iconEmoji: ':myIconEmoji:',
team: 'myTeam',
text: 'myText',
channel: 'myChannel',
botName: 'myBot'
}
const {iconEmoji, team, text, channel, botName} = integrationConfig
pushToSlack(channel, text, botName, iconEmoji, team, etc);
Spread syntax is not usable for that
use Destructuring assignment
integrationConfig =
{ iconEmoji : ':myIconEmoji:'
, team : 'myTeam'
, text : 'myText'
, channel : 'myChannel'
, botName : 'myBot'
}
the call :
return await this.pushToSlack( integrationConfig, attachments);
the function :
exports.pushToSlack = function ({channel, text, botName, iconEmoji, team}, attachments, ...
//..Destructuring assignment....^.......................................^
// Arguments can be in any order you want
// and no obligation to have all of them
I want to check if an object already exists in a given object by only having the object.
For instance:
const information = {
...
city: {
Streetname: ''
}
}
Now, I get the city object and want to check if it is already in the information object (without knowing the property name). The city could be n deep in the information object.
To get the property name of an object you can use Object.keys(). The first problem solved.
Now we need to iterate through the whole object including nested objects. This is the second problem.
And compare it to a query object. This is the third problem.
I assume that we have an object that only contains "simple" though nested objects with primitive values (I do not consider objects with functions or arrays)
// let's assume we have this object
const information = {
city: {
Streetname: 'streetname1'
},
house: {
color: "blue",
height: 100,
city: {
findMe: { Streetname: '' } // we want to get the path to this property 'findMe'
}
},
findMeToo: {
Streetname: '' // we also want to get the path to this proeprty 'findMeToo'
},
willNotFindMe: {
streetname: '' // case sensetive
}
}
// this is our object we want to use to find the property name with
const queryObject = {
Streetname : ''
}
If you use === to compare Objects you will always compare by reference. In our case, we are interested to compare the values. There is a rather extensive checking involved if you want to do it for more complex objects (read this SO comment for details), we will use a simplistic version:
// Note that this only evaluates to true if EVERYTHING is equal.
// This includes the order of the properties, since we are eventually comparing strings here.
JSON.stringify(obj1) === JSON.stringify(obj2)
Before we start to implement our property pathfinder I will introduce a simple function to check if a given value is an Object or a primitive value.
function isObject(obj) {
return obj === Object(obj); // if you pass a string it will create an object and compare it to a string and thus result to false
}
We use this function to know when to stop diving deeper since we reached a primitive value which does not contain any further objects. We loop through the whole object and dive deeper every time we find a nested object.
function findPropertyPath(obj, currentPropertyPath) {
const keys = isObject(obj) ? Object.keys(obj) : []; // if it is not an Object we want to assign an empty array or Object.keys() will implicitly cast a String to an array object
const previousPath = currentPropertyPath; // set to the parent node
keys.forEach(key => {
const currentObj = obj[key];
currentPropertyPath = `${previousPath}.${key}`;
if (JSON.stringify(currentObj) === JSON.stringify(queryObject)) console.log(currentPropertyPath); // this is what we are looking for
findPropertyPath(currentObj, currentPropertyPath); // since we are using recursion this is not suited for deeply nested objects
})
}
findPropertyPath(information, "information"); // call the function with the root key
This will find all "property paths" that contain an object that is equal to your query object (compared by value) using recursion.
information.house.city.findMe
information.findMeToo
const contains = (item, data) => item === data || Object.getOwnPropertyNames(data).some(prop => contains(item, data[prop]));
const information = {
city: {
Streetname: ''
}
}
console.log(contains(information.city, information));
console.log(contains({}, information));
New ES 6 (Harmony) introduces new Set object. Identity algorithm used by Set is similar to === operator and so not much suitable for comparing objects:
var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]
How to customize equality for Set objects in order to do deep object comparison? Is there anything like Java equals(Object)?
Update 3/2022
There is currently a proposal to add Records and Tuples (basically immutable Objects and Arrays) to Javascript. In that proposal, it offers direct comparison of Records and Tuples using === or !== where it compares values, not just object references AND relevant to this answer both Set and Map objects would use the value of the Record or Tuple in key comparisons/lookups which would solve what is being asked for here.
Since the Records and Tuples are immutable (can't be modified) and because they are easily compared by value (by their contents, not just their object reference), it allows Maps and Sets to use object contents as keys and the proposed spec explicitly names this feature for Sets and Maps.
This original question asked for customizability of a Set comparison in order to support deep object comparison. This doesn't propose customizability of the Set comparison, but it directly supports deep object comparison if you use the new Record or a Tuple instead of an Object or an Array and thus would solve the original problem here.
Note, this proposal advanced to Stage 2 in mid-2021. It has been moving forward recently, but is certainly not done.
Mozilla work on this new proposal can be tracked here.
Original Answer
The ES6 Set object does not have any compare methods or custom compare extensibility.
The .has(), .add() and .delete() methods work only off it being the same actual object or same value for a primitive and don't have a means to plug into or replace just that logic.
You could presumably derive your own object from a Set and replace .has(), .add() and .delete() methods with something that did a deep object comparison first to find if the item is already in the Set, but the performance would likely not be good since the underlying Set object would not be helping at all. You'd probably have to just do a brute force iteration through all existing objects to find a match using your own custom compare before calling the original .add().
Here's some info from this article and discussion of ES6 features:
5.2 Why can’t I configure how maps and sets compare keys and values?
Question: It would be nice if there were a way to configure what map
keys and what set elements are considered equal. Why isn’t there?
Answer: That feature has been postponed, as it is difficult to
implement properly and efficiently. One option is to hand callbacks to
collections that specify equality.
Another option, available in Java, is to specify equality via a method
that object implement (equals() in Java). However, this approach is
problematic for mutable objects: In general, if an object changes, its
“location” inside a collection has to change, as well. But that’s not
what happens in Java. JavaScript will probably go the safer route of
only enabling comparison by value for special immutable objects
(so-called value objects). Comparison by value means that two values
are considered equal if their contents are equal. Primitive values are
compared by value in JavaScript.
As mentioned in jfriend00's answer customization of equality relation is probably not possible.
Following code presents an outline of computationally efficient (but memory expensive) workaround:
class GeneralSet {
constructor() {
this.map = new Map();
this[Symbol.iterator] = this.values;
}
add(item) {
this.map.set(item.toIdString(), item);
}
values() {
return this.map.values();
}
delete(item) {
return this.map.delete(item.toIdString());
}
// ...
}
Each inserted element has to implement toIdString() method that returns string. Two objects are considered equal if and only if their toIdString methods returns same value.
As the top answer mentions, customizing equality is problematic for mutable objects. The good news is (and I'm surprised no one has mentioned this yet) there's a very popular library called immutable-js that provides a rich set of immutable types which provide the deep value equality semantics you're looking for.
Here's your example using immutable-js:
const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]
Maybe you can try to use JSON.stringify() to do deep object comparison.
for example :
const arr = [
{name:'a', value:10},
{name:'a', value:20},
{name:'a', value:20},
{name:'b', value:30},
{name:'b', value:40},
{name:'b', value:40}
];
const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);
console.log(result);
To add to the answers here, I went ahead and implemented a Map wrapper that takes a custom hash function, a custom equality function, and stores distinct values that have equivalent (custom) hashes in buckets.
Predictably, it turned out to be slower than czerny's string concatenation method.
Full source here: https://github.com/makoConstruct/ValueMap
Comparing them directly seems not possible, but JSON.stringify works if the keys just were sorted. As I pointed out in a comment
JSON.stringify({a:1, b:2}) !== JSON.stringify({b:2, a:1});
But we can work around that with a custom stringify method. First we write the method
Custom Stringify
Object.prototype.stringifySorted = function(){
let oldObj = this;
let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
let type = typeof (oldObj[key])
if (type === 'object') {
obj[key] = oldObj[key].stringifySorted();
} else {
obj[key] = oldObj[key];
}
}
return JSON.stringify(obj);
}
The Set
Now we use a Set. But we use a Set of Strings instead of objects
let set = new Set()
set.add({a:1, b:2}.stringifySorted());
set.has({b:2, a:1}.stringifySorted());
// returns true
Get all the values
After we created the set and added the values, we can get all values by
let iterator = set.values();
let done = false;
while (!done) {
let val = iterator.next();
if (!done) {
console.log(val.value);
}
done = val.done;
}
Here's a link with all in one file
http://tpcg.io/FnJg2i
For Typescript users the answers by others (especially czerny) can be generalized to a nice type-safe and reusable base class:
/**
* Map that stringifies the key objects in order to leverage
* the javascript native Map and preserve key uniqueness.
*/
abstract class StringifyingMap<K, V> {
private map = new Map<string, V>();
private keyMap = new Map<string, K>();
has(key: K): boolean {
let keyString = this.stringifyKey(key);
return this.map.has(keyString);
}
get(key: K): V {
let keyString = this.stringifyKey(key);
return this.map.get(keyString);
}
set(key: K, value: V): StringifyingMap<K, V> {
let keyString = this.stringifyKey(key);
this.map.set(keyString, value);
this.keyMap.set(keyString, key);
return this;
}
/**
* Puts new key/value if key is absent.
* #param key key
* #param defaultValue default value factory
*/
putIfAbsent(key: K, defaultValue: () => V): boolean {
if (!this.has(key)) {
let value = defaultValue();
this.set(key, value);
return true;
}
return false;
}
keys(): IterableIterator<K> {
return this.keyMap.values();
}
keyList(): K[] {
return [...this.keys()];
}
delete(key: K): boolean {
let keyString = this.stringifyKey(key);
let flag = this.map.delete(keyString);
this.keyMap.delete(keyString);
return flag;
}
clear(): void {
this.map.clear();
this.keyMap.clear();
}
size(): number {
return this.map.size;
}
/**
* Turns the `key` object to a primitive `string` for the underlying `Map`
* #param key key to be stringified
*/
protected abstract stringifyKey(key: K): string;
}
Example implementation is then this simple: just override the stringifyKey method. In my case I stringify some uri property.
class MyMap extends StringifyingMap<MyKey, MyValue> {
protected stringifyKey(key: MyKey): string {
return key.uri.toString();
}
}
Example usage is then as if this was a regular Map<K, V>.
const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);
const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair
myMap.size(); // returns 1, not 2
A good stringification method for the special but frequent case of a TypedArray as Set/Map key is using
const key = String.fromCharCode(...new Uint16Array(myArray.buffer));
It generates the shortest possible unique string that can be easily converted back. However this is not always a valid UTF-16 string for display concerning Low and High Surrogates. Set and Map seem to ignore surrogate validity.
As measured in Firefox and Chrome, the spread operator performs slowly. If your myArray has fixed size, it executes faster when you write:
const a = new Uint16Array(myArray.buffer); // here: myArray = Uint32Array(2) = 8 bytes
const key = String.fromCharCode(a[0],a[1],a[2],a[3]); // 8 bytes too
Probably the most valuable advantage of this method of key-building: It works for Float32Array and Float64Array without any rounding side-effect. Note that +0 and -0 are then different. Infinities are same. Silent NaNs are same. Signaling NaNs are different depending on their signal (never seen in vanilla JavaScript).
As other guys said there is no native method can do it by far.
But if you would like to distinguish an array with your custom comparator, you can try to do it with the reduce method.
function distinct(array, equal) {
// No need to convert it to a Set object since it may give you a wrong signal that the set can work with your objects.
return array.reduce((p, c) => {
p.findIndex((element) => equal(element, c)) > -1 || p.push(c);
return p;
}, []);
}
// You can call this method like below,
const users = distinct(
[
{id: 1, name: "kevin"},
{id: 2, name: "sean"},
{id: 1, name: "jerry"}
],
(a, b) => a.id === b.id
);
...
As others have said, there is no way to do it with the current version of Set.
My suggestion is to do it using a combination of arrays and maps.
The code snipped below will create a map of unique keys based on your own defined key and then transform that map of unique items into an array.
const array =
[
{ "name": "Joe", "age": 17 },
{ "name": "Bob", "age": 17 },
{ "name": "Carl", "age": 35 }
]
const key = 'age';
const arrayUniqueByKey = [...new Map(array.map(item =>
[item[key], item])).values()];
console.log(arrayUniqueByKey);
/*OUTPUT
[
{ "name": "Bob", "age": 17 },
{ "name": "Carl", "age": 35 }
]
*/
// Note: this will pick the last duplicated item in the list.
To someone who found this question on Google (as me) wanting to get a value of a Map using an object as Key:
Warning: this answer will not work with all objects
var map = new Map<string,string>();
map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");
console.log(map.get(JSON.stringify({"A":2}))||"Not worked");
Output:
Worked
What is the point of the 'Symbol' type in ECMA-262-v6? Fast path implementation for object keys? What does it do under the hood - hash it with the guarantee that the underlying data is immutable?
Symbols are private keys that replace magic names. They prevent using a simple string to reference the field, so only consumers with the symbol can gain access.
Some symbols are used to indicate particular behaviors to the runtime (like Symbol.iterator, which acts much like a pre-shared secret), while others can be allocated by the library and used to effectively hide fields.
In general, symbols are intended as a replacement for magical names. Rather than having a properties simply called 'foo', you can allocate a symbol const foo = Symbol() and pass that selectively. This allows the runtime to allocate Symbol.iterator when it starts up and guarantees that anyone trying to implement an iterable does so in a consistent fashion.
The runtime could use symbols to optimize access to certain fields, if it felt the need to, but doesn't have to.
You can use symbols to direct consumers to a particular method, depending on their usage. For example, if you had a library that could return a synchronous iterable or a generator, depending on the client's async support, you could:
const syncIterable = Symbol();
const asyncIterable = Symbol();
class Foo {
static getIterable(async = false) {
return async ? asyncIterable : syncIterable;
}
[syncIterable]() {
return new SyncFoo();
}
[asyncIterable]() {
return new AsyncFoo();
}
}
let foo = new Foo();
for (let x of foo[Foo.getIterable(true)]()) {
// could be a iterator, could be a generator
}
That's a rather contrived example, but shows how a library can use symbols to selectively provide access to users.
They pretty much help us having naming collisions. Anytime that you want to create a property in a unique way, that's when you should reach for a symbol.
Take a look at my example
const bert = Symbol('Bert');
'Bert'
Note: this is not a value this is what they called a descriptor,
because the symbol itself is just a unique identifier. So if you were
to visualize what a symbol would be maybe you can visualize is as
something like this "sdfasdfa2342134987fgsdfgsdf9808fsfgsd" absolute
unique symbol so that you can make sure that it will never overrides
any other piece of code in there.
What's cool about this is if I create a second symbol, like
const person = Symbol('Bert')
You can see I used 'Bert' again. Are those going to be the same because I described them as the same thing?
const bert = Symbol('Bert');
const person = Symbol('Bert');
console.log(bert);
console.log(person);
console.log(bert === person);
console.log(bert == person);
This can be useful if you were creating an object of your class.
const classRoom = {
'Mia' : { grade: 50, gender: 'female' },
'Gilbert': { grade: 80, gender: 'male' },
'Gilbert' { grade: 80, gender: 'male' },
};
But then you have another named Gilbert, so you got a naming collision there. So imagine if you're working on million and millions of data. So rather than using the persons name or using some sort of unique identifier we can use a symbol to name them.
const classRoom = {
[Symbol('Mia')] : { grade: 50, gender: 'female' },
[Symbol('Gilbert')]: { grade: 80, gender: 'male' },
[Symbol('Gilbert')]: { grade: 80, gender: 'male' },
};
Another thing about symbols is that they are not enumerable, which means we cannot loop over them if I were to do
for (const person in classRoom) {
console.log(person);
}
I get nothing.
If you do want to get access to all your symbols because theyres some information that you want to get you can use the object method.
const syms = Object.getOwnPropertySymbols(classRoom);
const data = syms.map(sym => classRoom[sym]);
console.log(data);
New ES 6 (Harmony) introduces new Set object. Identity algorithm used by Set is similar to === operator and so not much suitable for comparing objects:
var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]
How to customize equality for Set objects in order to do deep object comparison? Is there anything like Java equals(Object)?
Update 3/2022
There is currently a proposal to add Records and Tuples (basically immutable Objects and Arrays) to Javascript. In that proposal, it offers direct comparison of Records and Tuples using === or !== where it compares values, not just object references AND relevant to this answer both Set and Map objects would use the value of the Record or Tuple in key comparisons/lookups which would solve what is being asked for here.
Since the Records and Tuples are immutable (can't be modified) and because they are easily compared by value (by their contents, not just their object reference), it allows Maps and Sets to use object contents as keys and the proposed spec explicitly names this feature for Sets and Maps.
This original question asked for customizability of a Set comparison in order to support deep object comparison. This doesn't propose customizability of the Set comparison, but it directly supports deep object comparison if you use the new Record or a Tuple instead of an Object or an Array and thus would solve the original problem here.
Note, this proposal advanced to Stage 2 in mid-2021. It has been moving forward recently, but is certainly not done.
Mozilla work on this new proposal can be tracked here.
Original Answer
The ES6 Set object does not have any compare methods or custom compare extensibility.
The .has(), .add() and .delete() methods work only off it being the same actual object or same value for a primitive and don't have a means to plug into or replace just that logic.
You could presumably derive your own object from a Set and replace .has(), .add() and .delete() methods with something that did a deep object comparison first to find if the item is already in the Set, but the performance would likely not be good since the underlying Set object would not be helping at all. You'd probably have to just do a brute force iteration through all existing objects to find a match using your own custom compare before calling the original .add().
Here's some info from this article and discussion of ES6 features:
5.2 Why can’t I configure how maps and sets compare keys and values?
Question: It would be nice if there were a way to configure what map
keys and what set elements are considered equal. Why isn’t there?
Answer: That feature has been postponed, as it is difficult to
implement properly and efficiently. One option is to hand callbacks to
collections that specify equality.
Another option, available in Java, is to specify equality via a method
that object implement (equals() in Java). However, this approach is
problematic for mutable objects: In general, if an object changes, its
“location” inside a collection has to change, as well. But that’s not
what happens in Java. JavaScript will probably go the safer route of
only enabling comparison by value for special immutable objects
(so-called value objects). Comparison by value means that two values
are considered equal if their contents are equal. Primitive values are
compared by value in JavaScript.
As mentioned in jfriend00's answer customization of equality relation is probably not possible.
Following code presents an outline of computationally efficient (but memory expensive) workaround:
class GeneralSet {
constructor() {
this.map = new Map();
this[Symbol.iterator] = this.values;
}
add(item) {
this.map.set(item.toIdString(), item);
}
values() {
return this.map.values();
}
delete(item) {
return this.map.delete(item.toIdString());
}
// ...
}
Each inserted element has to implement toIdString() method that returns string. Two objects are considered equal if and only if their toIdString methods returns same value.
As the top answer mentions, customizing equality is problematic for mutable objects. The good news is (and I'm surprised no one has mentioned this yet) there's a very popular library called immutable-js that provides a rich set of immutable types which provide the deep value equality semantics you're looking for.
Here's your example using immutable-js:
const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]
Maybe you can try to use JSON.stringify() to do deep object comparison.
for example :
const arr = [
{name:'a', value:10},
{name:'a', value:20},
{name:'a', value:20},
{name:'b', value:30},
{name:'b', value:40},
{name:'b', value:40}
];
const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);
console.log(result);
To add to the answers here, I went ahead and implemented a Map wrapper that takes a custom hash function, a custom equality function, and stores distinct values that have equivalent (custom) hashes in buckets.
Predictably, it turned out to be slower than czerny's string concatenation method.
Full source here: https://github.com/makoConstruct/ValueMap
Comparing them directly seems not possible, but JSON.stringify works if the keys just were sorted. As I pointed out in a comment
JSON.stringify({a:1, b:2}) !== JSON.stringify({b:2, a:1});
But we can work around that with a custom stringify method. First we write the method
Custom Stringify
Object.prototype.stringifySorted = function(){
let oldObj = this;
let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
let type = typeof (oldObj[key])
if (type === 'object') {
obj[key] = oldObj[key].stringifySorted();
} else {
obj[key] = oldObj[key];
}
}
return JSON.stringify(obj);
}
The Set
Now we use a Set. But we use a Set of Strings instead of objects
let set = new Set()
set.add({a:1, b:2}.stringifySorted());
set.has({b:2, a:1}.stringifySorted());
// returns true
Get all the values
After we created the set and added the values, we can get all values by
let iterator = set.values();
let done = false;
while (!done) {
let val = iterator.next();
if (!done) {
console.log(val.value);
}
done = val.done;
}
Here's a link with all in one file
http://tpcg.io/FnJg2i
For Typescript users the answers by others (especially czerny) can be generalized to a nice type-safe and reusable base class:
/**
* Map that stringifies the key objects in order to leverage
* the javascript native Map and preserve key uniqueness.
*/
abstract class StringifyingMap<K, V> {
private map = new Map<string, V>();
private keyMap = new Map<string, K>();
has(key: K): boolean {
let keyString = this.stringifyKey(key);
return this.map.has(keyString);
}
get(key: K): V {
let keyString = this.stringifyKey(key);
return this.map.get(keyString);
}
set(key: K, value: V): StringifyingMap<K, V> {
let keyString = this.stringifyKey(key);
this.map.set(keyString, value);
this.keyMap.set(keyString, key);
return this;
}
/**
* Puts new key/value if key is absent.
* #param key key
* #param defaultValue default value factory
*/
putIfAbsent(key: K, defaultValue: () => V): boolean {
if (!this.has(key)) {
let value = defaultValue();
this.set(key, value);
return true;
}
return false;
}
keys(): IterableIterator<K> {
return this.keyMap.values();
}
keyList(): K[] {
return [...this.keys()];
}
delete(key: K): boolean {
let keyString = this.stringifyKey(key);
let flag = this.map.delete(keyString);
this.keyMap.delete(keyString);
return flag;
}
clear(): void {
this.map.clear();
this.keyMap.clear();
}
size(): number {
return this.map.size;
}
/**
* Turns the `key` object to a primitive `string` for the underlying `Map`
* #param key key to be stringified
*/
protected abstract stringifyKey(key: K): string;
}
Example implementation is then this simple: just override the stringifyKey method. In my case I stringify some uri property.
class MyMap extends StringifyingMap<MyKey, MyValue> {
protected stringifyKey(key: MyKey): string {
return key.uri.toString();
}
}
Example usage is then as if this was a regular Map<K, V>.
const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);
const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair
myMap.size(); // returns 1, not 2
A good stringification method for the special but frequent case of a TypedArray as Set/Map key is using
const key = String.fromCharCode(...new Uint16Array(myArray.buffer));
It generates the shortest possible unique string that can be easily converted back. However this is not always a valid UTF-16 string for display concerning Low and High Surrogates. Set and Map seem to ignore surrogate validity.
As measured in Firefox and Chrome, the spread operator performs slowly. If your myArray has fixed size, it executes faster when you write:
const a = new Uint16Array(myArray.buffer); // here: myArray = Uint32Array(2) = 8 bytes
const key = String.fromCharCode(a[0],a[1],a[2],a[3]); // 8 bytes too
Probably the most valuable advantage of this method of key-building: It works for Float32Array and Float64Array without any rounding side-effect. Note that +0 and -0 are then different. Infinities are same. Silent NaNs are same. Signaling NaNs are different depending on their signal (never seen in vanilla JavaScript).
As other guys said there is no native method can do it by far.
But if you would like to distinguish an array with your custom comparator, you can try to do it with the reduce method.
function distinct(array, equal) {
// No need to convert it to a Set object since it may give you a wrong signal that the set can work with your objects.
return array.reduce((p, c) => {
p.findIndex((element) => equal(element, c)) > -1 || p.push(c);
return p;
}, []);
}
// You can call this method like below,
const users = distinct(
[
{id: 1, name: "kevin"},
{id: 2, name: "sean"},
{id: 1, name: "jerry"}
],
(a, b) => a.id === b.id
);
...
As others have said, there is no way to do it with the current version of Set.
My suggestion is to do it using a combination of arrays and maps.
The code snipped below will create a map of unique keys based on your own defined key and then transform that map of unique items into an array.
const array =
[
{ "name": "Joe", "age": 17 },
{ "name": "Bob", "age": 17 },
{ "name": "Carl", "age": 35 }
]
const key = 'age';
const arrayUniqueByKey = [...new Map(array.map(item =>
[item[key], item])).values()];
console.log(arrayUniqueByKey);
/*OUTPUT
[
{ "name": "Bob", "age": 17 },
{ "name": "Carl", "age": 35 }
]
*/
// Note: this will pick the last duplicated item in the list.
To someone who found this question on Google (as me) wanting to get a value of a Map using an object as Key:
Warning: this answer will not work with all objects
var map = new Map<string,string>();
map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");
console.log(map.get(JSON.stringify({"A":2}))||"Not worked");
Output:
Worked