fp-search-algorithms
Version:
Functional Programming Style Search Algorithms and Unordered Containers
1,528 lines (1,520 loc) • 73.2 kB
JavaScript
/* eslint-disable complexity */
const unequalDates = (a, b) => {
return a instanceof Date && (a > b || a < b);
};
const unequalBuffers = (a, b) => {
return (a.buffer instanceof ArrayBuffer &&
a.BYTES_PER_ELEMENT &&
!(a.byteLength === b.byteLength && a.every((n, i) => n === b[i])));
};
const unequalArrays = (a, b) => {
return Array.isArray(a) && a.length !== b.length;
};
const unequalMaps = (a, b) => {
return a instanceof Map && a.size !== b.size;
};
const unequalSets = (a, b) => {
return a instanceof Set && (a.size !== b.size || [...a].some(e => !b.has(e)));
};
const unequalRegExps = (a, b) => {
return a instanceof RegExp && (a.source !== b.source || a.flags !== b.flags);
};
const isObject = (a) => {
return typeof a === 'object' && a !== null;
};
const structurallyCompatibleObjects = (a, b) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (typeof a !== 'object' && typeof b !== 'object' && (!a || !b))
return false;
const nonstructural = [Promise, WeakSet, WeakMap, Function];
if (nonstructural.some(c => a instanceof c))
return false;
return a.constructor === b.constructor;
};
/**
* Check for equality by structure of two values
* * Returns true if strict equality (`===`) returns true
* * Values of different `typeof` return `false`
* * Objects with different constructors return `false`
* * Dates return true if both `>` and `<` return false
* * ArrayBuffers return true when byteLength are equal and if values at all indexes are equal
* * Arrays return true when lengths are equal and when values at all indexes pass `isEqual()` recursively
* * Sets returns true when both are empty or when all keys equal on both
* * Maps returns true when both are empty, when all keys equal on both, and when those key's values pass `isEqual()` recursively
* * Dispatches to first argument's prototype method `equals: (other) => boolean` if exists
* * Objects return true when both share same enumerable keys and all key's values pass `isEqual()` recursively
*
* Exceptions:
* * Functions, Promises, WeakSets, and WeakMaps are checked by reference
*
* Notes:
* * `isEqual({}, Object.create(null))` will always be `false`, regardless of keys/values because they don't share the same constructor
*
* @category Helpers
* @returns boolean indicating whether the values are equal in value, structure, or reference
*/
const isEqual = (x, y) => {
const values = [x, y];
while (values.length) {
const a = values.pop();
const b = values.pop();
if (a === b)
continue;
if (!isObject(a) || !isObject(b))
return false;
const unequal = !structurallyCompatibleObjects(a, b) ||
unequalDates(a, b) ||
unequalBuffers(a, b) ||
unequalArrays(a, b) ||
unequalMaps(a, b) ||
unequalSets(a, b) ||
unequalRegExps(a, b);
if (unequal)
return false;
const proto = Object.getPrototypeOf(a);
if (proto !== null && typeof proto.equals === 'function') {
try {
if (a.equals(b))
continue;
else
return false;
}
catch {
// fall-through
}
}
if (a instanceof Map) {
if (!(b instanceof Map))
return false;
for (const k of a.keys()) {
values.push(a.get(k), b.get(k));
}
}
else {
// assume a and b are objects
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
const bKeysSet = new Set(bKeys);
if (aKeys.length !== bKeys.length)
return false;
const extra = a instanceof globalThis.Error ? ['message'] : [];
for (const k of [...extra, ...aKeys]) {
// @ts-expect-error
values.push(a[k], b[k]);
bKeysSet.delete(k);
}
if (bKeysSet.size)
return false;
}
}
return true;
};
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable complexity */
/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
/* eslint-disable prefer-arrow/prefer-arrow-functions */
/* eslint-disable func-style */
//
// Credit to: https://github.com/gleam-lang/stdlib/blob/main/src/dict.mjs
// Ported to typescript
//
const referenceMap = new WeakMap();
const tempDataView = new DataView(new ArrayBuffer(8));
let referenceUID = 0;
/**
* hash the object by reference using a weak map and incrementing uid
*/
const hashByReference = (o) => {
const known = referenceMap.get(o);
if (known !== undefined) {
return known;
}
const hash = referenceUID++;
if (referenceUID === 0x7fffffff) {
referenceUID = 0;
}
referenceMap.set(o, hash);
return hash;
};
/**
* merge two hashes in an order sensitive way
*/
const hashMerge = (a, b) => (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0;
/**
* standard string hash popularized by java
*/
const hashString = (s) => {
let hash = 0;
const len = s.length;
for (let i = 0; i < len; i++) {
hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0;
}
return hash;
};
/**
* hash a number by converting to two integers and do some jumbling
*/
const hashNumber = (n) => {
tempDataView.setFloat64(0, n);
const i = tempDataView.getInt32(0);
const j = tempDataView.getInt32(4);
return Math.imul(0x45d9f3b, (i >> 16) ^ i) ^ j;
};
/**
* hash a BigInt by converting it to a string and hashing that
*/
const hashBigInt = (n) => hashString(n.toString());
/**
* hash any js object
*/
const hashObject = (o) => {
const proto = Object.getPrototypeOf(o);
if (proto !== null && typeof proto.hashCode === 'function') {
try {
const code = o.hashCode(o);
if (typeof code === 'number') {
return code;
}
// eslint-disable-next-line no-empty
}
catch { }
}
if (o instanceof Promise || o instanceof WeakSet || o instanceof WeakMap) {
return hashByReference(o);
}
if (o instanceof Date) {
return hashNumber(o.getTime());
}
let h = 0;
if (o instanceof ArrayBuffer) {
o = new Uint8Array(o);
}
if (Array.isArray(o) || o instanceof Uint8Array) {
for (let i = 0; i < o.length; i++) {
h = (Math.imul(31, h) + getHash(o[i])) | 0;
}
}
else if (o instanceof Set) {
o.forEach(v => {
h = (h + getHash(v)) | 0;
});
}
else if (o instanceof Map) {
o.forEach((v, k) => {
h = (h + hashMerge(getHash(v), getHash(k))) | 0;
});
}
else {
const keys = Object.keys(o);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = o[k];
h = (h + hashMerge(getHash(v), hashString(k))) | 0;
}
}
return h;
};
/**
* hash any js value
*/
function getHash(u) {
if (u === null)
return 0x42108422;
if (u === undefined)
return 0x42108423;
if (u === true)
return 0x42108421;
if (u === false)
return 0x42108420;
switch (typeof u) {
case 'number':
return hashNumber(u);
case 'string':
return hashString(u);
case 'bigint':
return hashBigInt(u);
case 'object':
return hashObject(u);
case 'symbol':
return hashByReference(u);
case 'function':
return hashByReference(u);
default:
throw new Error('getHash - non-exhaustive switch statement');
}
}
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
/* eslint-disable prefer-arrow/prefer-arrow-functions */
/* eslint-disable func-style */
//
// This file is a fork of: https://github.com/gleam-lang/stdlib/blob/main/src/dict.mjs
// * ported to typescript
// * originally written with full immutability for Gleam, but I updated it to do in-place mutations for performance gains
// * My HashMap and HashSet are intentionally mutable to match that characteristic of the native Map and Set
//
const SHIFT = 5; // number of bits you need to shift by to get the next bucket
const BUCKET_SIZE = 2 ** SHIFT;
const MASK = BUCKET_SIZE - 1; // used to zero out all bits not in the bucket
const MAX_INDEX_NODE = BUCKET_SIZE / 2; // when does index node grow into array node
const MIN_ARRAY_NODE = BUCKET_SIZE / 4; // when does array node shrink to index node
const ENTRY = 0;
const ARRAY_NODE = 1;
const INDEX_NODE = 2;
const COLLISION_NODE = 3;
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createEmptyNode = () => ({
array: [],
bitmap: 0,
type: INDEX_NODE,
});
/**
* Mask the hash to get only the bucket corresponding to shift
* @internal
*/
function mask(hash, shift) {
return (hash >>> shift) & MASK;
}
/**
* Set only the Nth bit where N is the masked hash
* @internal
*/
function bitpos(hash, shift) {
return 1 << mask(hash, shift);
}
/**
* Count the number of 1 bits in a number
*/
function bitcount(x) {
x -= (x >> 1) & 0x55555555;
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x + (x >> 4)) & 0x0f0f0f0f;
x += x >> 8;
x += x >> 16;
return x & 0x7f;
}
/**
* Calculate the array index of an item in a bitmap index node
*/
function index(bitmap, bit) {
return bitcount(bitmap & (bit - 1));
}
/**
* Create a new node containing two entries
*/
function createNode(shift, key1, val1, key2hash, key2, val2) {
const key1hash = getHash(key1);
if (key1hash === key2hash) {
return {
array: [
{ k: key1, type: ENTRY, v: val1 },
{ k: key2, type: ENTRY, v: val2 },
],
hash: key1hash,
type: COLLISION_NODE,
};
}
const addedLeaf = { val: false };
return assoc(assocIndex(createEmptyNode(), shift, key1hash, key1, val1, addedLeaf), shift, key2hash, key2, val2, addedLeaf);
}
/**
* Associate a node with a new entry, creating a new node
* @internal
*/
function assoc(root, shift, hash, key, val, addedLeaf) {
switch (root.type) {
case ARRAY_NODE:
return assocArray(root, shift, hash, key, val, addedLeaf);
case INDEX_NODE:
return assocIndex(root, shift, hash, key, val, addedLeaf);
case COLLISION_NODE:
return assocCollision(root, shift, hash, key, val, addedLeaf);
default:
throw new Error('function assoc :: non-exhaustive');
}
}
function assocArray(root, shift, hash, key, val, addedLeaf) {
const idx = mask(hash, shift);
const node = root.array[idx];
// if the corresponding index is empty set the index to a newly created node
if (node === undefined) {
addedLeaf.val = true;
root.array[idx] = { k: key, type: ENTRY, v: val };
root.size += 1;
return root;
}
if (node.type === ENTRY) {
// if keys are equal replace the entry
if (isEqual(key, node.k)) {
if (val === node.v)
return root;
root.array[idx] = { k: key, type: ENTRY, v: val };
return root;
}
// otherwise upgrade the entry to a node and insert
addedLeaf.val = true;
root.array[idx] = createNode(shift + SHIFT, node.k, node.v, hash, key, val);
return root;
}
// otherwise call assoc on the child node
root.array[idx] = assoc(node, shift + SHIFT, hash, key, val, addedLeaf);
return root;
}
function assocIndex(root, shift, hash, key, val, addedLeaf) {
const bit = bitpos(hash, shift);
const idx = index(root.bitmap, bit);
// if there is already a item at this hash index..
if ((root.bitmap & bit) !== 0) {
// if there is a node at the index (not an entry), call assoc on the child node
const node = root.array[idx];
if (node.type !== ENTRY) {
const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf);
root.array[idx] = n;
return root;
}
// otherwise there is an entry at the index
// if the keys are equal replace the entry with the updated value
const nodeKey = node.k;
if (isEqual(key, nodeKey)) {
if (val === node.v)
return root;
node.v = val;
return root;
}
// if the keys are not equal, replace the entry with a new child node
addedLeaf.val = true;
root.array[idx] = createNode(shift + SHIFT, nodeKey, node.v, hash, key, val);
root.type = INDEX_NODE;
return root;
}
// else there is currently no item at the hash index
const n = root.array.length;
// if the number of nodes is at the maximum, expand this node into an array node
if (n >= MAX_INDEX_NODE) {
// create a 32 length array for the new array node (one for each bit in the hash)
const nodes = new Array(32);
// create and insert a node for the new entry
const jdx = mask(hash, shift);
nodes[jdx] = assocIndex(createEmptyNode(), shift + SHIFT, hash, key, val, addedLeaf);
let j = 0;
let { bitmap } = root;
// place each item in the index node into the correct spot in the array node
// loop through all 32 bits / array positions
for (let i = 0; i < 32; i++) {
if ((bitmap & 1) !== 0) {
const node = root.array[j++];
nodes[i] = node;
}
// shift the bitmap to process the next bit
bitmap >>>= 1;
}
return {
array: nodes,
size: n + 1,
type: ARRAY_NODE,
};
}
// else there is still space in this index node
// simply insert a new entry at the hash index
addedLeaf.val = true;
const newEntryNode = {
k: key,
type: ENTRY,
v: val,
};
root.array.splice(idx, 0, newEntryNode);
root.bitmap |= bit;
return root;
}
function assocCollision(root, shift, hash, key, val, addedLeaf) {
// if there is a hash collision
if (hash === root.hash) {
const idx = collisionIndexOf(root, key);
// if this key already exists replace the entry with the new value
if (idx !== -1) {
const entry = root.array[idx];
if (entry.v === val)
return root;
root.array[idx] = { k: key, type: ENTRY, v: val };
return root;
}
// otherwise insert the entry at the end of the array
addedLeaf.val = true;
root.array.push({ k: key, type: ENTRY, v: val });
return root;
}
// if there is no hash collision, upgrade to an index node
return assoc({
array: [root],
bitmap: bitpos(root.hash, shift),
type: INDEX_NODE,
}, shift, hash, key, val, addedLeaf);
}
/**
* Find the index of a key in the collision node's array
*/
function collisionIndexOf(root, key) {
const size = root.array.length;
for (let i = 0; i < size; i++) {
if (isEqual(key, root.array[i].k)) {
return i;
}
}
return -1;
}
/**
* Return the found entry or undefined if not present in the root
* @internal
*/
function find(root, shift, hash, key) {
switch (root.type) {
case ARRAY_NODE:
return findArray(root, shift, hash, key);
case INDEX_NODE:
return findIndex(root, shift, hash, key);
case COLLISION_NODE:
return findCollision(root, key);
default:
throw new Error('function find :: non-exhaustive');
}
}
function findArray(root, shift, hash, key) {
const idx = mask(hash, shift);
const node = root.array[idx];
if (node === undefined) {
return undefined;
}
if (node.type !== ENTRY) {
return find(node, shift + SHIFT, hash, key);
}
if (isEqual(key, node.k)) {
return node;
}
return undefined;
}
function findIndex(root, shift, hash, key) {
const bit = bitpos(hash, shift);
if ((root.bitmap & bit) === 0) {
return undefined;
}
const idx = index(root.bitmap, bit);
const node = root.array[idx];
if (node.type !== ENTRY) {
return find(node, shift + SHIFT, hash, key);
}
if (isEqual(key, node.k)) {
return node;
}
return undefined;
}
function findCollision(root, key) {
const idx = collisionIndexOf(root, key);
if (idx < 0) {
return undefined;
}
return root.array[idx];
}
/**
* Remove an entry from the root, returning the updated root.
* Returns undefined if the node should be removed from the parent.
* @internal
*/
function without(root, shift, hash, key) {
switch (root.type) {
case ARRAY_NODE:
return withoutArray(root, shift, hash, key);
case INDEX_NODE:
return withoutIndex(root, shift, hash, key);
case COLLISION_NODE:
return withoutCollision(root, key);
default:
throw new Error('function without :: non-exhaustive');
}
}
function withoutArray(root, shift, hash, key) {
const idx = mask(hash, shift);
const node = root.array[idx];
if (node === undefined)
return root; // already empty
let n;
// if node is an entry and the keys are not equal there is nothing to remove
// if node is not an entry do a recursive call
if (node.type === ENTRY) {
if (!isEqual(node.k, key)) {
return root; // no changes
}
}
else {
n = without(node, shift + SHIFT, hash, key);
}
// if ENTRY and isEqual, or the recursive call returned undefined, the node should be removed
if (n === undefined) {
// if the number of child nodes is at the minimum, pack into an index node
if (root.size <= MIN_ARRAY_NODE) {
const arr = root.array;
const out = new Array(root.size - 1);
let i = 0;
let j = 0;
let bitmap = 0;
while (i < idx) {
const nv = arr[i];
if (nv !== undefined) {
out[j] = nv;
bitmap |= 1 << i;
++j;
}
++i;
}
++i; // skip copying the removed node
while (i < arr.length) {
const nv = arr[i];
if (nv !== undefined) {
out[j] = nv;
bitmap |= 1 << i;
++j;
}
++i;
}
return {
array: out,
bitmap,
type: INDEX_NODE,
};
}
root.array[idx] = n;
root.size -= 1;
return root;
}
root.array[idx] = n;
return root;
}
function withoutIndex(root, shift, hash, key) {
const bit = bitpos(hash, shift);
if ((root.bitmap & bit) === 0)
return root; // already empty
const idx = index(root.bitmap, bit);
const node = root.array[idx];
// if the item is not an entry
if (node.type !== ENTRY) {
const n = without(node, shift + SHIFT, hash, key);
// if not undefined, the child node still has items, so update it
if (n !== undefined) {
root.array[idx] = n;
return root;
}
// otherwise the child node should be removed
// if it was the only child node, remove this node from the parent
if (root.bitmap === bit)
return undefined;
// otherwise just remove the child node
root.array.splice(idx, 1);
root.bitmap ^= bit;
return root;
}
// otherwise the item is an entry, remove it if the key matches
if (isEqual(key, node.k)) {
// if it was the only child node, remove this node from the parent
if (root.bitmap === bit)
return undefined;
root.array.splice(idx, 1);
root.bitmap ^= bit;
return root;
}
return root;
}
function withoutCollision(root, key) {
const idx = collisionIndexOf(root, key);
// if the key not found, no changes
if (idx < 0)
return root;
// otherwise the entry was found, remove it
// if it was the only entry in this node, remove the whole node
if (root.array.length === 1)
return undefined;
// otherwise just remove the entry
root.array.splice(idx, 1);
return root;
}
/** @internal */
function forEach(root, fn) {
if (root === undefined) {
return;
}
const items = root.array;
const size = items.length;
for (let i = 0; i < size; i++) {
const item = items[i];
if (item === undefined) {
continue;
}
if (item.type === ENTRY) {
fn(item.v, item.k);
continue;
}
forEach(item, fn);
}
}
/** @internal */
function toArray(root) {
const array = [];
forEach(root, (v, k) => array.push([k, v]));
return array;
}
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/unified-signatures */
/* eslint-disable @typescript-eslint/prefer-return-this-type */
/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
//
// Inspired by: https://github.com/gleam-lang/stdlib/blob/main/src/dict.mjs
// Ported to typescript
//
/**
* ### HashMap
*
* Key equality is determined by `isEqual`
*
* If your keys are Javascript [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), there is no benefit in using a HashMap over the native [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map).
*
* #### Construction
* `HashMap` is a newable class, and has the static `HashMap.from` functional constructor for convenience
* * In addition to the arguments the new constructor accepts, `HashMap.from()` also accepts objects directly
*
* #### Native Map API
* The HashMap API fully implements the Map API and can act as a drop-in replacement with a few caveats:
* * Non-primitive Map keys are equal by reference, a Map may contain two different keys that share the same structure. A HashMap will not see those keys as being different.
* * Order of insertion is not retained.
*
* Those methods are:
* * `clear`, `delete`, `entries`, `forEach`, `get`, `has`, `keys`, `set`, `values`, `[Symbol.Iterator]`
* * static `groupBy`
* * readonly prop `size`
*
* #### Array API
* HashMap partially implements the Array API, specifically the reduction methods that can apply.
* The callbackfn signatures match their array equivalent with `k: Key` replacing the `index: number` argument.
*
* Those methods are:
* * `map`, `filter`, `find`, `reduce`, `some`, `every`
* * Notes:
* * `map` and `filter` are immutable, returning a new instance of HashMap
* * `find` returns a tuple `[K, V] | undefined`
*
* #### Additional Utility APIs
* * `clone` - will return a new instance of a HashMap with the exact same key/value pair entries
* * `equals` - determine if another HashMap is equal to `this` by determining they share the same key/value pair entries
* * Therefor `hashMap.equals(hashMap.clone())` will always return `true`
*
* #### Custom Equality and Hashing
* Internally, class instances who's prototypes implement `equals: (other: typeof this) => boolean` and `hashCode(self: typeof this) => number`
* will be used to determine equality between instances and an instance's hash value respectively.
* It is recommended to implement these on any class where equality cannot be determined testing on public properties only
*
* @category Structures
*/
class HashMap {
root;
_size;
static from(oneOfThem) {
if (oneOfThem == null) {
return new HashMap();
}
if (Symbol.iterator in oneOfThem) {
return new HashMap(oneOfThem);
}
// else isObject
return new HashMap(Object.entries(oneOfThem));
}
constructor(iterable) {
this.root = undefined;
this._size = 0;
if (iterable != null) {
for (const [k, v] of iterable) {
this.set(k, v);
}
}
}
//#endregion
//#region Utility
/**
* Groups members of an iterable according to the return value of the passed callback.
* @group Utility
* @param items An iterable.
* @param keySelector A callback which will be invoked for each item in items.
*/
static groupBy(items, keySelector) {
const dict = new HashMap();
let i = 0;
for (const val of items) {
const key = keySelector(val, i);
if (!dict.has(key)) {
dict.set(key, []);
}
dict.get(key).push(val);
++i;
}
return dict;
}
/**
* @group Utility
* @returns an immutable copy of the HashMap
*/
clone() {
const clone = new HashMap();
clone.root = structuredClone(this.root);
clone._size = this.size;
return clone;
}
/**
* Check if this HashMap is equal to another
* Returns `true` when
* * referentially equal
* * both HashMaps contain exactly the same key/value pairs
* * both are empty
*
* @group Utility
* @param other another HashMap
* @returns boolean indicating whether the other HashMap has the exactly same entries as this
*/
equals(other) {
if (this === other)
return true;
if (!(other instanceof HashMap) || this._size !== other._size)
return false;
for (const [k, v] of this) {
if (!isEqual(other.get(k), v)) {
return false;
}
}
return true;
}
/**
* Used internally by `getHash()`
* @group Utility
* @returns the hash of this HashMap
*/
hashCode() {
let h = 0;
this.forEach((v, k) => {
h = (h + hashMerge(getHash(v), getHash(k))) | 0;
});
return h;
}
//#endregion
//#region Entries
/**
* @group Entries
* @returns the number of elements in the HashMap.
*/
get size() {
return this._size;
}
/**
* Empties the HashMap, clearing out all entries.
* @group Entries
*/
clear() {
this.root = undefined;
this._size = 0;
}
/**
* @group Entries
* @returns true if an element in the HashMap existed and has been removed, or false if the element does not exist.
*/
delete(key) {
if (this.root === undefined)
return false;
if (!this.has(key))
return false;
this.root = without(this.root, 0, getHash(key), key);
this._size -= 1;
return true;
}
/**
* Executes a provided function once per each key/value pair in the Map, in insertion order.
* @group Entries
*/
forEach(fn) {
forEach(this.root, fn);
}
/**
* Returns a specified element from the HashMap. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the HashMap.
* @group Entries
* @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
*/
get(key) {
if (this.root === undefined)
return undefined;
const found = find(this.root, 0, getHash(key), key);
return found?.v;
}
/**
* @group Entries
* @returns boolean indicating whether an element with the specified key exists or not.
*/
has(key) {
if (this.root === undefined)
return false;
return find(this.root, 0, getHash(key), key) !== undefined;
}
/**
* Adds a new element with a specified key and value to the HashMap. If an element with the same key already exists, the element will be updated.
* @group Entries
*/
set(key, val) {
const addedLeaf = { val: false };
const root = this.root ?? createEmptyNode();
this.root = assoc(root, 0, getHash(key), key, val, addedLeaf);
this._size = addedLeaf.val ? this._size + 1 : this._size;
return this;
}
//#endregion
//#region Iterables
/**
* Returns an iterable of entries in the HashMap.
* @group Iterables
*/
[Symbol.iterator]() {
return this.entries();
}
/**
* Returns an iterable of key, value pairs for every entry in the HashMap.
* @group Iterables
*/
entries() {
return Iterator.from(toArray(this.root));
}
/**
* Returns an iterable of keys in the HashMap.
* @group Iterables
*/
keys() {
return this.entries().map(([k]) => k);
}
/**
* Returns an iterable of values in the HashMap.
* @group Iterables
*/
values() {
return this.entries().map(([, v]) => v);
}
map(callbackfn, thisArg) {
const hashMap = new HashMap();
this.forEach((v, k) => {
hashMap.set(k, callbackfn.call(thisArg, v, k, this));
});
return hashMap;
}
filter(predicate, thisArg) {
const hashMap = new HashMap();
this.forEach((v, k) => {
if (predicate.call(thisArg, v, k, this)) {
hashMap.set(k, v);
}
});
return hashMap;
}
find(predicate, thisArg) {
for (const [k, v] of this.entries()) {
if (predicate.call(thisArg, v, k, this)) {
return [k, v];
}
}
return undefined;
}
reduce(callbackfn, initialValue) {
if (arguments.length === 1 && this.size === 0) {
throw new TypeError('Reduce of empty HashMap with no initial value');
}
let entries = Array.from(this.entries());
let acc;
if (arguments.length === 1) {
const [head, ...rest] = entries;
acc = head[1];
entries = rest;
}
else {
acc = initialValue;
}
for (const [k, v] of entries) {
acc = callbackfn(acc, v, k, this);
}
return acc;
}
some(predicate, thisArg) {
for (const [k, v] of this.entries()) {
if (predicate.call(thisArg, v, k, this))
return true;
}
return false;
}
/**
* Determines whether all the entries of a HashMap satisfy the specified test.
*
* @group Reductions
* @param predicate A function that accepts up to three arguments. The every method calls
* the predicate function for each element in the array until the predicate returns a value
* which is coercible to the Boolean value false, or until the end of the HashMap iteration.
*/
every(predicate, thisArg) {
for (const [k, v] of this.entries()) {
if (!predicate.call(thisArg, v, k, this))
return false;
}
return true;
}
}
/* eslint-disable no-bitwise */
// get parent index (intDiv(i, 2))
const parent = (i) => ((i + 1) >>> 1) - 1;
// double + 1
const left = (i) => (i << 1) + 1;
// double + 2
const right = (i) => (i + 1) << 1;
/* eslint-enable no-bitwise */
/**
*
* @category Structures
*/
class PriorityQueue {
comparator;
equals;
heap = [];
constructor(comparator, equals = (a, b) => a === b) {
this.comparator = comparator;
this.equals = equals;
}
size() {
return this.heap.length;
}
isEmpty() {
return this.size() === 0;
}
peek() {
return this.heap[0];
}
has(other) {
return this.heap.find(x => this.equals(x, other)) != null;
}
replace(value) {
const replacedValue = this.peek();
this.heap[0] = value;
this.siftDown();
return replacedValue;
}
push(value) {
this.heap.push(value);
this.siftUp();
}
pop() {
const poppedValue = this.peek();
const bottom = this.size() - 1;
if (bottom > 0) {
this.swap(0, bottom);
}
this.heap.pop();
this.siftDown();
return poppedValue;
}
reorder() {
const l = this.heap.length;
const original = [...this.heap];
this.heap = [];
for (let i = 0; i < l; i += 1) {
this.push(original[i]);
}
}
toArray() {
const copy = [...this.heap];
const arr = [];
while (this.size() > 0) {
arr.push(this.pop());
}
this.heap = copy;
return arr;
}
greater(i, j) {
return this.comparator(this.heap[i], this.heap[j]);
}
swap = (i, j) => {
const a = this.heap[i];
const b = this.heap[j];
this.heap[j] = a;
this.heap[i] = b;
};
siftUp() {
let i = this.size() - 1;
while (i > 0 && this.greater(i, parent(i))) {
this.swap(i, parent(i));
i = parent(i);
}
}
siftDown() {
let i = 0;
while ((left(i) < this.size() && this.greater(left(i), i)) ||
(right(i) < this.size() && this.greater(right(i), i))) {
const maxChild = right(i) < this.size() && this.greater(right(i), left(i)) ? right(i) : left(i);
this.swap(i, maxChild);
i = maxChild;
}
}
}
/** @internal */
const createPath = (prevMap, final) => {
const path = [final];
let prev = prevMap.get(final);
while (prev != null) {
path.unshift(prev);
prev = prevMap.get(prev);
}
return path;
};
/**
* Generator function that lazily iterates through each visit of an A* search.
* If you want just the found path and totalCost to the solution, use `aStarAssoc`
*
* Each yield is an object `{ cost: number; path: T[] }`
*
* Notes:
* * The first yield will be the initialState with a cost of 0
* * Specific states may be visited multiple time, but through different costs and paths
* * If the solved state is found, that will be the final yield, otherwise the final yield will happen once all possible states are visited
* * Generator `return` value (at `done: true`) will be the found solution or undefined
*
* @category AStar
* @param getNextStates - a function to generate list of neighboring states with associated transition costs given the current state
* @param estimateRemainingCost - a heuristic function to determine remaining cost
* @param determineIfFound - a function to determine if solution found
* @param initial - initial state
*/
const generateAStarAssoc = function* (getNextStates, estimateRemainingCost, determineIfFound, initial) {
const cameFrom = new HashMap();
const gScore = new HashMap().set(initial, 0);
const fScore = new HashMap().set(initial, estimateRemainingCost(initial));
const queue = new PriorityQueue((a, b) => {
const aScore = fScore.get(a);
const bScore = fScore.get(b);
return aScore < bScore;
}, isEqual);
queue.push(initial);
while (!queue.isEmpty()) {
const state = queue.pop();
const cost = gScore.get(state);
const toYield = {
cost,
path: createPath(cameFrom, state),
};
yield toYield;
if (determineIfFound(state))
return toYield;
const nextStates = getNextStates(state);
for (const [nextState, nextCost] of nextStates) {
const tentativeGScore = cost + nextCost;
if (tentativeGScore < (gScore.get(nextState) ?? Infinity)) {
cameFrom.set(nextState, state);
gScore.set(nextState, tentativeGScore);
fScore.set(nextState, tentativeGScore + estimateRemainingCost(nextState));
if (queue.has(nextState)) {
queue.reorder();
}
else {
queue.push(nextState);
}
}
}
}
return undefined;
};
/**
* Generator function that lazily iterates through each visit of an A* search.
* If you want just the found path and totalCost to the solution, use `aStar`
*
* Each yield is an object `{ cast: number; path: T[] }`
*
* Notes:
* * The first yield will be the initialState with a cost of 0
* * Specific states may be visited multiple time, but through different costs and paths
* * If the solved state is found, that will be the final yield, otherwise the final yield will happen once all possible states are visited
* * The return value is the total cost and path, or undefined if path to solved state is not possible
*
* @category AStar
* @param getNextStates - a function to generate list of neighboring states given the current state
* @param getCost - a function to generate transition costs between neighboring states
* @param estimateRemainingCost - a heuristic function to determine remaining cost
* @param determineIfFound - a function to determine if solution found
* @param initial - initial state
*/
const generateAStar = function* (getNextStates, getCost, estimateRemainingCost, determineIfFound, initial) {
const nextAssoc = (state) => getNextStates(state).map(n => [n, getCost(state, n)]);
return yield* generateAStarAssoc(nextAssoc, estimateRemainingCost, determineIfFound, initial);
};
/**
* Performs a best-first search using the A* search algorithm
*
* @category AStar
* @param getNextStates - a function to generate list of neighboring states with associated transition costs given the current state
* @param estimateRemainingCost - a heuristic function to determine remaining cost
* @param determineIfFound - a function to determine if solution found
* @param initial - initial state
* @returns an object with `totalCost` and the `path` with costs between states, or `undefined` if no path found
*/
const aStarAssoc = (getNextStates, estimateRemainingCost, determineIfFound, initial) => {
const iterable = generateAStarAssoc(getNextStates, estimateRemainingCost, determineIfFound, initial);
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = iterable.next();
if (done === true)
return value;
}
};
/**
* Performs a best-first search using the A* search algorithm
*
* @category AStar
* @param getNextStates - a function to generate list of neighboring states given the current state
* @param getCost - a function to generate transition costs between neighboring states
* @param estimateRemainingCost - a heuristic function to determine remaining cost
* @param determineIfFound - a function to determine if solution found
* @param initial - initial state
* @returns an object with `totalCost` and the `path` with costs between states, or `undefined` if no path found
*/
const aStar = (getNextStates, getCost, estimateRemainingCost, determineIfFound, initial) => {
const iterable = generateAStar(getNextStates, getCost, estimateRemainingCost, determineIfFound, initial);
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = iterable.next();
if (done === true)
return value;
}
};
/* eslint-disable @typescript-eslint/unified-signatures */
/**
* ### HashSet
*
* Value equality is determined by `isEqual`
*
* If your values are Javascript [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), there is no benefit in using a HashSet over the native [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
*
* #### Construction
* `HashSet` is a newable class, and has the static `HashSet.from` functional constructor for convenience and accepts the same arguments
*
* #### Native Set API
* The HashSet API fully implements the Set API and can act as a drop-in replacement with a few caveats:
* * Non-primitive Set values are equal by reference, a Set may contain two different values that share the same structure. A HashSet will not see those values as being different
* * Order of insertion is not retained
*
* Those methods are:
* * `clear`, `delete`, `entries`, `forEach`, `add`, `has`, `keys`, `values`, `[Symbol.Iterator]`
* * readonly prop `size`
*
* #### Set Composition API
*
* The hashSet API also fully implements the new Composition API methods with one caveat:
* * Unlike the Set methods which all except "set-like" objects, HashSet _only_ accepts another HashSet in their arguments
*
* Those methods are:
* * `difference`, `intersection`, `isDisjointFrom`, `isSubsetOf`, `isSupersetOf`, `symmetricDifference`, `union`
*
* #### Array API
* HashSet partially implements the Array API, specifically the reduction methods that can apply.
* The callbackfn signatures match their array equivalent but without the `index: number` argument
*
* Those methods are:
* * `map`, `filter`, `find`, `reduce`, `some`, `every`
* * Notes:
* * `map` and `filter` are immutable, returning a new instance of HashSet
*
* #### Additional Utility APIs
* * `clone` - will return a new instance of a HashSet with the exact same values
* * `equals` - determine if another HashSet is equal to `this` by determining they share the same values
* * Therefor `hashSet.equals(hashSet.clone())` will always return `true`
*
* #### Custom Equality and Hashing
* Internally, class instances who's prototypes implement `equals: (other: typeof this) => boolean` and `hashCode(self: typeof this) => number`
* will be used to determine equality between instances and an instance's hash value respectively.
* It is recommended to implement these on any class where equality cannot be determined testing on public properties only
*
* @category Structures
*/
class HashSet {
dict;
static from(iterable) {
return new HashSet(iterable);
}
constructor(iterable) {
this.dict = new HashMap();
if (iterable != null) {
for (const v of iterable) {
this.add(v);
}
}
}
//#endregion
//#region Utility
/**
* Create an immutable copy of the HashSet
* @group Utility
*/
clone() {
const set = new HashSet();
set.dict = this.dict.clone();
return set;
}
/**
* Check if this HashSet is equal to another
* Returns `true` when
* * referentially equal
* * both HashSets contain exactly the same values
* * both are empty
*
* @group Utility
*/
equals(other) {
return this.dict.equals(other.dict);
}
/**
* Used internally by `getHash()`
* @group Utility
*/
hashCode() {
// @ts-expect-error
return this.dict.hashCode();
}
//#endregion
//#region Entries
/**
* @group Entries
* @returns the number of (unique) elements in Set.
*/
get size() {
return this.dict.size;
}
/**
* Adds a new element with a specified value to the HashSet.
* @group Entries
*/
add(val) {
this.dict.set(val, undefined);
return this;
}
/**
* * Empties the HashSet, clearing out all values.
* @group Entries
*/
clear() {
this.dict.clear();
}
/**
* Removes a specified value from the HashSet.
* @group Entries
* @returns Returns true if an element in the HashSet existed and has been removed, or false if the element does not exist.
*/
delete(val) {
return this.dict.delete(val);
}
/**
* @group Entries
* @returns a boolean indicating whether an element with the specified value exists in the HashSet or not.
*/
has(val) {
return this.dict.has(val);
}
/**
* Executes a provided function once per each value in the HashSet object.
* @group Entries
*/
forEach(fn) {
this.dict.forEach((_v, k) => {
fn(k);
});
}
//#endregion
//#region Iterables
/**
* Iterates over values in the HashSet
* @group Iterables
*/
[Symbol.iterator]() {
return this.keys();
}
/**
* Returns an iterable of [v,v] pairs for every value `v` in the HashSet.
* @group Iterables
*/
entries() {
return this.dict.keys().map(k => [k, k]);
}
/**
* Despite its name, returns an iterable of the values in the HashSet.
* @group Iterables
*/
keys() {
return this.dict.keys();
}
/**
* Returns an iterable of values in the HashSet.
* @group Iterables
*/
values() {
return this.dict.keys();
}
//#endregion
//#region Composition
/**
* @group Composition
* @returns a new HashSet containing all the elements in this HashSet which are not also in the argument.
*/
difference(other) {
const [iter, check] = (this.size > other.size ? [other, this] : [this, other]);
const set = this.clone();
for (const v of iter) {
if (check.has(v)) {
set.delete(v);
}
}
return set;
}
/**
* @group Composition
* @returns a new HashSet containing all the elements which are both in this HashSet and in the argument.
*/
intersection(other) {
const [iter, check] = (this.size > other.size ? [other, this] : [this, other]);
const set = new HashSet();
for (const v of iter) {
if (check.has(v)) {
set.add(v);
}
}
return set;
}
/**
* @group Composition
* @returns a boolean indicating whether this HashSet has no elements in common with the argument.
*/
isDisjointFrom(other) {
const [iter, check] = this.size > other.size ? [other, this] : [this, other];
for (const v of iter) {
if (check.has(v))
return false;
}
return true;
}
/**
* @group Composition
* @returns a boolean indicating whether all the elements in this HashSet are also in the argument.
*/
isSubsetOf(other) {
for (const v of this) {
if (!other.has(v))
return false;
}
return true;
}
/**
* @group Composition
* @returns a boolean indicating whether all the elements in the argument are also in this HashSet.
*/
isSupersetOf(other) {
for (const v of other) {
if (!this.has(v))
return false;
}
return true;
}
/**
* @group Composition
* @returns a new HashSet containing all the elements which are in either this HashSet or in the argument, but not in both.
*/
symmetricDifference(other) {
const set = this.clone();
for (const v of other) {
if (set.has(v)) {
set.delete(v);
}
else {
set.add(v);
}
}
return set;
}
/**
* @group Composition
* @returns a new HashSet containing all the elements in this HashSet and also all the elements in the argument.
*/
union(other) {
const set = this.clone();
const iter = other.keys();
let visit = iter.next();
while (!(visit.done ?? false)) {
if (!set.has(visit.value)) {
set.add(visit.value);
}
visit = iter.next();
}
return set;
}
map(callbackfn, thisArg) {
const hashSet = new HashSet();
this.forEach(v => {
hashSet.add(callbackfn.call(thisArg, v, this));
});
return hashSet;
}
filter(predicate, thisArg) {
const hashSet = new HashSet();
this.forEach(v => {
if (predicate.call(thisArg, v, this)) {
hashSet.add(v);
}
});
return hashSet;
}
find(predicate, thisArg) {
for (const v of this.values()) {
if (predicate.call(thisArg, v, this)) {
return v;
}
}
return undefined;
}
reduce(callbackfn, initialValue) {
if (arguments.length === 1 && this.size === 0) {
throw new TypeError('Reduce of empty HashSet with no initial value');
}
let values = Array.from(this.values());
let acc;
if (arguments.length === 1) {
const [head, ...rest] = values;
acc = head;
values = rest;
}
else {
acc = initialValue;
}
for (const v of values) {
acc = callbackfn(acc, v, this);
}
return acc;
}
some(predicate, thisArg) {
for (const v of this.values()) {
if (predicate.call(thisArg, v, this))
return true;
}
return false;
}
/**
* Determines whether all the values of a HashSet satisfy the specified test.
*
* @group Reductions
* @param predicate A function that accepts up to two arguments. The every method calls
* the predicate function for each value in the array until the predicate returns a value
* which is coercible to the Boolean value false, or until the end of the HashSet iteration.
*/
every(predicate, thisArg) {