How can I check if two Map objects are equal? - javascript

How can I check if two ES2015 Map objects have the same set of (key, value) pairs?
We can assume that all the keys and values are primitive datatypes.
One approach to solve this would be to take the map.entries(), create array from it, then sort that array by keys. And do the same thing with the other map. And then loop through those two arrays to compare them. All this seams cumbersome and also very inefficient because of sorting (performance inefficiency) and because of making those arrays (memory inefficiency).
Does anybody have better idea?

There is no "standard" or "built-in" way to do this. Conceptually, you just have to compare that the two Map objects have the same keys and values for each key and have no extra keys.
To be as efficient about the comparison as possible, you can do the following optimizations:
First check the .size property on both maps. If the two maps don't have the same number of keys, then you know right away, they can't be identical.
Furthermore, guaranteeing that they have the same number of keys allows you to just iterate one of the maps and compare its values to the other.
Use the for (var [key, val] of map1) iterator syntax for iterating the keys so you don't have to build or sort an array of keys yourself (should be both faster and more memory efficient).
Then, lastly, if you make sure that the comparison returns immediately as soon as a mismatch is found, then it will shorten the execution time when they are not the same.
Then, since undefined is a legal value in a Map, but it's also what .get() returns if the key is not found, we have to watch out for that by doing an extra .has() if the value we're comparing is undefined.
Since both keys and values with a Map object can be objects themselves, this gets much trickier if you want a deep property comparison of objects to determine equality rather than just the more simple === that Javascript uses by default to test for the same object. Or, if you're only interested in objects that have primitives for keys and values, then this complexity can be avoided.
For a function that tests only strict value equality (checks objects to see if they are the same physical object, not a deep property comparison), you can do what is shown below. This uses ES6 syntax for efficient iteration of the map objects and attempts to improve performance when they do not match by short circuiting and returning false as soon as a mismatch is found.
"use strict";
function compareMaps(map1, map2) {
let testVal;
if (map1.size !== map2.size) {
return false;
}
for (let [key, val] of map1) {
testVal = map2.get(key);
// in cases of an undefined value, make sure the key
// actually exists on the object so there are no false positives
if (testVal !== val || (testVal === undefined && !map2.has(key))) {
return false;
}
}
return true;
}
// construct two maps that are initially identical
const o = {"k" : 2}
const m1 = new Map();
m1.set("obj", o);
m1.set("str0", undefined);
m1.set("str1", 1);
m1.set("str2", 2);
m1.set("str3", 3);
const m2 = new Map();
m2.set("str0", undefined);
m2.set("obj", o);
m2.set("str1", 1);
m2.set("str2", 2);
m2.set("str3", 3);
log(compareMaps(m1, m2));
// add an undefined key to m1 and a corresponding other key to m2
// this will pass the .size test and even pass the equality test, but not pass the
// special test for undefined values
m1.set("str-undefined", undefined);
m2.set("str4", 4);
log(compareMaps(m1, m2));
// remove one key from m1 so m2 has an extra key
m1.delete("str-undefined");
log(compareMaps(m1, m2));
// add that same extra key to m1, but give it a different value
m1.set("str4", 5);
log(compareMaps(m1, m2));
function log(args) {
let str = "";
for (let i = 0; i < arguments.length; i++) {
if (typeof arguments[i] === "object") {
str += JSON.stringify(arguments[i]);
} else {
str += arguments[i];
}
}
const div = document.createElement("div");
div.innerHTML = str;
const target = log.id ? document.getElementById(log.id) : document.body;
target.appendChild(div);
}
If you wanted to do deep object comparison rather than just comparing to see if they are physically the same object, where values could be objects or arrays, then life gets a lot more complicated.
To do that, you need a deep object comparison method that takes into account all of the following:
Recursive comparison for nested objects
Protection against circular references (which can cause an infinite loop)
Knowledge of how to compare some types of built-in objects such as a Date.
Since a lot has been written elsewhere about how to do a deep object comparison (including a number of highly voted answers here on StackOverflow), I will assume that is not the main part of your question.

If your Map has only string keys, then you can use this approach to compare them:
const mapToObj = (map) => {
let obj = Object.create(null)
for (let [k,v] of map) {
// We don’t escape the key '__proto__'
// which can cause problems on older engines
obj[k] = v
}
return obj
}
assert.deepEqual(mapToObj(myMap), myExpectedObj)
Note: deepEqual is part of many testing suites and if not, you can use lodash/underscore equivalents. Any function that does a deep comparison will do.
mapToObj function courtesy of http://exploringjs.com/es6/ch_maps-sets.html

Here's a one-liner function for checking map equality:
const mapsAreEqual = (m1, m2) => m1.size === m2.size && Array.from(m1.keys()).every((key) => m1.get(key) === m2.get(key));

The above won't work for Map<string, object> as in the following line will not evaluate two objects properly:
if (testVal !== val || (testVal === undefined && !map2.has(key))) {
The following version extends the function for Map<string, object> by comparing using JSON.stringify()
function compareMaps(map1, map2) {
var testVal;
if (map1.size !== map2.size) {
return false;
}
for (var [key, val] of map1) {
testVal = map2.get(key);
// in cases of an undefined value, make sure the key
// actually exists on the object so there are no false positives
if (JSON.stringify(testVal) !== JSON.stringify(val) || (testVal === undefined && !map2.has(key))) {
return false;
}
}
return true;
}

Here is my example with the ability to provide an optional compare function
/**
* The utility function that returns an intersection of two Sets
*
* #returns an array of items in common
*/
function intersection<T>(a: Set<T>, b: Set<T>): T[] {
return Array.from(a).filter(x => b.has(x));
}
/**
* Compares two Maps
*
* #param compare is an optional function for values comparison
* #returns `true` if they are equal, and `false` otherwise
*/
function compareMaps<T>(a: Map<string, T>, b: Map<string, T>, compare?: (aValue: T, bValue: T) => boolean): boolean {
const common = intersection(new Set(a.keys()), new Set(b.keys()));
return a.size === b.size &&
common.length === a.size &&
common.every(key =>
compare?.(a.get(key) as T, b.get(key) as T) ?? a.get(key) === b.get(key));
}

Note that the solution proposed by #jfriend00 does not work in typescript ES2016. See this for the right answer: iteration over a typescript map failing

Related

Comparable custom types which can be sorted as array items without defining an additional compare function inside sort()

Languages such as Python and Java have special methods for sorting custom classes. In JavaScript, toString() can be overridden, but this does not work easily for numeric values.
As a workaround, I added a method called compareTo() to the class, although this still requires a function to call it.
class NumericSortable {
constructor(newVal) {
this.val = newVal
}
compareTo(other) {
return this.val - other.val
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
]
objectList.sort(
function(a, b) { return a.compareTo(b) })
console.log(objectList)
Is there a way to modify the class so it can be sorted without requiring a function to be defined inside sort()?
Perhaps there is a good way to override toString() that will work for numeric values. However, solutions such as localeCompare() or a collator require two arguments, and they would not be passed to the overridden toString() method at the same time.
You can add a static method to your NumericSortable class, and pass that into the sort call. This idea can be extended to any custom class that need to define how two instances are to be compared for sorting.
class NumericSortable {
constructor(value) {
this.value = value;
}
static compare(a,b) {
return a.value - b.value;
}
}
const arr = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
];
arr.sort(NumericSortable.compare);
console.log(arr);
This makes things more explicit, and easier for anyone else reading the code to reason about how the array is being sorted.
I like to make a function that returns a sort function for these cases.
function by(prop){
return function(a,b){return a[prop]-b[prop];};
}
this let's you specify the object's to-be-sorted property at call-time, letting one generic function do a lot of heavy lifting.
objectList.sort(by("val"))
This avoids the need for a custom callback each sort, though with fat arrows that's not the burden it used to be anyway...
If no comparator is provided, each time two items in the array are compared, they'll be coerced to strings, and then those strings will be compared lexiographically to determine which object will come before the other in the sorted array. So, if you don't want to pass a comparator, adding a toString method to implement the desired sorting logic is the only other approach.
Unfortunately, for your situation, lexiographic comparison alone based on the .vals won't cut it; 1 will come before 20, and 20 will come before 3. If the numbers involved won't get so high as to have es in their string version, you could .padStart the returned string so that each compared numeric string will have the same number of characters (thereby allowing lexiographic comparison to work).
class NumericSortable {
constructor(newVal) {
this.val = newVal
}
// Unused now
compareTo(other) {
return this.val - other.val
}
toString() {
return String(this.val).padStart(15, '0');
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
]
objectList.sort()
console.log(objectList)
You may wish to account for negative numbers too.
Still, this whole approach is a bit smelly. When possible, I'd prefer a comparator instead of having to fiddle with the strings to get them to compare properly.
From my above comment ...
"The OP needs to wrap the build-in sort method into an own/custom implementation of Array.prototype.sort. But why should one do it? Just for the sake of not writing a comparing sort-callback?"
Having said the above, I herby nevertheless provide an implementation of the above mentioned approach just in order to prove to the OP that it's manageable (after all it is exactly what the OP did ask for), but also to show to the audience that the effort (even though it's a one time effort) of doing so is much greater than other already provided suggestions / solutions.
class NumericSortable {
constructor(newVal) {
this.val = newVal;
}
compareTo(other) {
return this.val - other.val;
}
}
const objectList = [
new NumericSortable(3),
new NumericSortable(1),
new NumericSortable(20),
];
objectList
// - sorting by overwritten custom `sort` function with
// an already build-in `compareTo` based custom compare
// function and no additionally passed compare function.
.sort();
console.log({ objectList });
objectList
// - reverse sorting by overwritten custom `sort` function
// with an additionally passed compare function.
.sort((a, b) => (-1 * a.compareTo(b)));
console.log({ objectList });
console.log(
'... simple sort-implementation defaul-test ...\n[1,4,9,0,6,3].sort() ...',
[1,4,9,0,6,3].sort()
);
console.log(
'... simple sort-implementation defaul-test ...\n["foo", "biz", "baz", "bar"].sort() ...',
["foo", "biz", "baz", "bar"].sort()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(function (arrProto) {
// save the native `sort` reference.
const coreApiSort = arrProto.sort;
// type detection helper.
function isFunction(value) {
return (
'function' === typeof value &&
'function' === typeof value.call &&
'function' === typeof value.apply
);
}
// different comparison helper functionality.
function defaultCompare(a, b) {
return (a < b && -1) || (a > b && 1) || 0;
}
function localeCompare(a, b) {
return a?.localeCompare?.(b) ?? defaultCompare(a, b);
}
function customDefaultCompare(a, b) {
const isCustomComparableA = isFunction(a.compareTo);
const isCustomComparableB = isFunction(b.compareTo);
return (isCustomComparableA && isCustomComparableB)
? a.compareTo(b)
: localeCompare(a, b);
}
// the new `sort` functionality.
function customSort(customCompare) {
return coreApiSort
// - (kind of "super") delegation to the
// before saved native `sort` reference ...
.call(this, (
// ... by using a cascade of different
// comparison functionality.
isFunction(customCompare)
&& customCompare
|| customDefaultCompare
));
}
Object
// - overwrite the Array prototype's native `sort`
// method with the newly implemented custom `sort`.
.defineProperty(arrProto, 'sort', {
value: customSort,
});
}(Array.prototype));
</script>

How to add objects in Set with having comparison on basis of any defined value of object in JavaScript? [duplicate]

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

How do I get Javascript Sets/Maps to test for object/array equality? [duplicate]

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

Simple way to force javascript to always return an array

I stumbled upon the YQL API to query for WOEIDs for use in Twitter, but I can see the output is not always in array. The API returns an object and I'm interested in value of response.query.results which returns the following:
if there are no results, it returns null
if there is only one result, it returns an object
if the are multiple results, it returns an array
I want the result to always be an array. I can solve this by checking the result using the following code:
var count = response.query.count;
if(count === 0) {
return [];
} else if(count === 1) {
var arr = [];
arr.push(response.query.results);
return arr;
} else {
return response.query.results;
}
Is there a javascript or lodash function that can simplify the above code? It seems _.forEach and _.toArray will treat each property as an object if provided with a single object.
You could use Array#concat with a default array if response.query.results is falsy.
return [].concat(response.query.results || []);
By having zero as value for response.query.results, you could take the Nullish coalescing operator ?? instead of logical OR ||, which repects all values without undefoned or null
return [].concat(response.query.results ?? []);
https://lodash.com/docs/4.17.4#concat
_.concat([],response.query.results);
would also do it.
but as #Brian pointed out, we need to handle null being equivalent to [] so you can add
_.concat([],_.isNull(response.query.results)?[]:response.query.results);
note that this is more correct because it will work for results with falsey values (like 0 and false etc)
in general, lodash is more robust than built in javascript. this usually works in your favour. one place this can trip you up is if results was a string (which is an array of characters)
https://github.com/lodash/lodash/blob/4.17.4/lodash.js#L6928
function concat() {
var length = arguments.length;
if (!length) {
return [];
}
var args = Array(length - 1),
array = arguments[0],
index = length;
while (index--) {
args[index - 1] = arguments[index];
}
return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
}
Similar to Tom's answer above, the lodash function castArray (https://lodash.com/docs/4.17.15#castArray), introduced in Lodash v4.4, could also work for this. It has the marginal benefit that its intent is slightly more clear that [].concat(x)
const _ = require('lodash')
_.castArray(null) // [null]
_.castArray({a:1}) // [{a:1}]
_.castArray([{a:1},{a:2}] // [{a:1},{a:2}]
To deal with the null, considerations are similar to answers above, depending on how you want to handle unexpected values. A ternary with _.isNull would work, or else ?? is useful, e.g.:
const castArrayRemovingNullUndef = x => _.castArray(x ?? [])
const castArrayRemovingNull = x => _.castArray(_.isNull(x) ? [] :x)
_.castArrayRemovingNull(null) // []
_.castArrayRemovingNull({a:1}) // [{a:1}]
_.castArrayRemovingNull([{a:1},{a:2}] // [{a:1},{a:2}]

How to identify anonymous types in JSON?

I am writing one function on Javascript which needs to address all the anynymous types in a JSON object.
For example,
Typed= {
emails: [{email:'a#a.com'}, {email:'b#a.com'}, {email:'c#a.com'}, {email:'d#a.com'}]
};
is an example of typed array in a JSON because each element inside the array is typed email
while,
Anon= {
emails: ['a#a.com', 'b#a.com', 'c#a.com', 'd#a.com']
};
is a JSON object where emails is collection of some anonymous objects.
Is there any ways that I can differentiate between both in JQuery or Javascript?
The simplest solution is to have the JSON source only return one of the two forms. Then you don't have to branch in your client.
If that's not an option, you could get the values out with JavaScript's handy lazy-evaluation of boolean expressions:
var em = json.emails[0].email || json.emails[0];
That statement will prefer the array-of-objects version, but use the array-of-strings version as a fallback.
(edited in response to clarifying comment below)
You can determine what properties a JS object has at runtime like this:
function enumerate(targetObject){
var props = [];
for (var propName in targetObject ){
props.push(propName);
}
return props;
}
console.log(enumerate({foo:1, bar:'baz'}),join(',')); //"foo, bar"
you could then modulate your logic on the basis of the properties you get back. You'll want to make sure you understand prototypes (specifically what Object.hasOwnProperty does and means), too.
You can use Array iteration methods to quickly check if all (or some) elements of the array have the desired type:
Anon.emails.every(function(e) { return typeof e == "object" }) // false
Typed.emails.every(function(e) { return typeof e == "object" }) // true
or a more generic solution
typeCheck = function(type) {
return function() {
return typeof arguments[0] == type
}
}
Anon.emails.every(typeCheck("object")) // false
Typed.emails.every(typeCheck("object")) // true
(An obligatory warning about iteration methods not being supported in ancient browsers)
How about this:
var istyped = function (a) {
if (typeof(a) !== 'object') {
return false;
}
var count = 0;
for (var key in a) {
count = count + 1;
}
return (count === 1);
}
I'm assuming here you just want to distinguish between regular variables (this would be your anonymous variable) and objects with just one key/value pair inside (this would be your typed variable).
To check if array contains only typed variables you'd just have to loop through it with that function. For example (in newer versions of JavaScript):
Typed.emails.every(istyped) = true
Anon.emails.every(istyped) = false
Why not do a map first:
emails = emails.map(function (email) {
if (typeof email.email === 'string')
return email.email;
});
That will make your emails array an array of just strings. Then you can just process it as usual. There aren't any side-effects if it is an array of strings (email.email will be undefined).
I do stuff like this when I have to make one client deal with multiple versions of an API. Alternatively, you could do the map the other way:
emails = emails.map(function (email) {
if (typeof email === 'string')
return {email: email};
});
This would work better if there could be other information in each object in your emails array.

Categories

Resources