Storing arrays in ES6 Set and accessing them by value
It's not using a Set
, but may solve your problem.
If you want to make sure that an array can only exist in a collection once, and you want instant lookups you can use a hash by abusing its keys. They do not necessarily need a value. You can set them to undefined
, to true
, or something else if it makes you sleep better at night ð.
Inspired by: javascript search array of arrays
It is halfway tempting to make a wrapper, but it is also simple enough to handle a small job.
const hash = {};
hash[[1, 2, 3]] = undefined;
hash[[3, 4]] = undefined;
hash[[3, 4]] = undefined;
console.log({hash});
console.log("hash has [3, 4]?", hash.hasOwnProperty([3, 4]));
For me, this will work fine, because I am checking if a coordinate exists in a collection. Unfortunately from this stackoverflow
it seems that Set
cannot be the answer to this problem.
Coincidentally it seems to implicitly solve your problem of not calling toString
on the array which this seems to do under the hood. Personally I don't care for hash.hasOwnProperty
.
A More Elegant Option
You could even write a wrapper to your code like:
class ArraySet extends Set {
add(arr) {
super.add(arr.toString());
}
has(arr) {
return super.has(arr.toString());
}
}
const arraySet = new ArraySet();
arraySet.add([1, 2]);
arraySet.add([3, 4]);
arraySet.add([3, 4]);
console.log("ArraySet has [3, 4]?", arraySet.has([3, 4]));
Too low rep, can't add comment so I'm adding it as an answer here. In advance, apologies for the wall of text.
I like CTS_AE's answer, and wanted to take the same route. However, there is one thing to note, and that is how are arrays put to String.
let memory = {};
memory[[1,2,3]] = "123";
console.log(memory[[1,2,3]]); // "123"
console.log([1,2,3].toString(); // "1,2,3"
console.log(memory["1,2,3"]); // "123"
Now this wouldn't be such an issue if you knew what you were putting in are exclusively arrays... or would it?
MDN about Array.prototype.toString() says
For Array objects, the toString method joins the array and returns one string containing each array element separated by commas.
Which brings 2 big issues:
- indexing via
array
is the same as indexing viaarray.toSring()
- since
toString()
is recursive, nested arrays end up being stringified into the same format as single-level arrays.
let memory = {};
memory[[1,2,3]] = "123";
console.log(memory[[1,2,3]]); // "123"
console.log(memory["1,2,3"]); // "123"
console.log(memory[[[1],[2],[3]]]); // "123"
console.log(memory[[[[1],2],3]]); // "123"
...and the list goes on. It's not hard to see when these issues can really break your project. I encountered such issue just a while ago when I tried memoizing
function doSomeStuff(string, sourceIdxs, targetIdxs) {
if (memo[[string, sourceIdxs, targetIdxs]])
return memo[[string, sourceIdxs, targetIdxs]];
// ...
}
In such case for example ["foo", [1, 3, 5], [6, 10]]
and ["foo", [1, 3], [5, 6, 10]]
point to the same value, and I ended up overwriting existing values, effectively corrupting the function memory.
Now, in the specific case of the ArraySet
answer above, the issue persists. While you don't mind if you overwrite existing key with another "same" key, you can end up getting false positives.
So how to fix this?
Option 1. stringify
The easy way out is using JSON.stringify()
to write the "exact" string representations of all the key data.
let memory = {};
memory[JSON.stringify([1,2,3])] = "123";
console.log(memory[JSON.stringify([1,2,3])]); // "123"
console.log(memory[JSON.stringify([[1],[2,3]])); // undefined
This helps with the false positives.... kind of. For one, you won't have issues with overlapping elements in the array. [[1,2],[3]]
no longer points to where [[1],[2,3]]
does.
However, [1,2,3]
and "[1,2,3]"
do. Also (in some bizarre cases), the array elements might contain [
or ]
characters, which might complicate the issue further.
And you might not care for such cases. Your keys may still be restrained well enough for such things not to happen. And you might even want such behaviour. If you do, go for it.
Good:
- Fixes most of issues of the
ArraySet
- Is easy to implement
Bad:
JSON.stringify()
is fairly slow.
Option 2. separators
Much simpler and quicker, while still giving a bit of advantage over the old solution.
function toSepString(arr) { return arr.join("|") }
let memory = {};
memory[toSepString([[1,2],3])] = "123";
console.log(memory[toSepString([1,[2,3]])]); // undefined
Now of course this helps only with the "outer-most" layer.
console.log(toSepString([1,[2,[3]]])); // "1|2,3"
console.log(toSepString([1,[[2],[[3]]]]); // "1|2,3"
So should you use this, you need to make sure the specific elements of your key array can't become ambiguous when converted to string.
Of course, you could make the function recursive and add "[", "]"
on each beginning and end, essentially copying a part of the functionality of JSON.stringify()
. I would imagine this would be still more performant than calling JSON.stringify()
directly, but that would require tests.
Good:
- faster than its built-in counterpart
Bad:
- issues with nested arrays
- issues with separator not appearing in values
No there is not.
A Set
works on objects and primitives and is useful for preventing identical primitives and re-adding the same object instance.
Each array is their own object, so you can actually add two different arrays with the same values.
var set = new Set();
set.add([3, 4]);
set.add([3, 4]);
console.log(set.size);//2
Additionally, there's nothing to prevent an object from being changed once in a set.
var set = new Set();
var a1 = [3, 4];
var a2 = [3, 4];
set.add(a1);
set.add(a2);
a2.push(5);
for (let a of set) {
console.log(a);
}
//Outputs:
// [3, 4]
// [3, 4, 5]
A set does not have a mechanism for checking the values of objects in a set. Since the value of an object could change at any time, it wouldn't be much more efficient than simply looping over them yourself.
The functionality you are looking has been kicked around in various ECMAScript proposals, however it does not appear to be coming anytime soon.