|
| 1 | +# Reactodia Hashmap [](https://badge.fury.io/js/@reactodia%2Fhashmap) |
| 2 | + |
| 3 | +`@reactodia/hashmap` is a TypeScript/JavaScript library that provides `HashMap` and `HashSet` collections to use as a compatible replacement for the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) when keys (items) are composite values. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +Install with: |
| 8 | +```sh |
| 9 | +npm install --save @reactodia/hashmap |
| 10 | +``` |
| 11 | + |
| 12 | +## Quick example |
| 13 | + |
| 14 | +```ts |
| 15 | +import { HashMap, hashTuple } from '@reactodia/hashmap'; |
| 16 | + |
| 17 | +type Edge = { from: string; to: string }; |
| 18 | +const hashEdge = (e: Edge) => hashTuple(e.from, e.to); |
| 19 | +const equalEdges = (a: Edge, b: Edge) => ( |
| 20 | + a.from === b.from && |
| 21 | + a.to === b.to |
| 22 | +); |
| 23 | + |
| 24 | +// Create a map with required hash and equality functions for keys |
| 25 | +const map = new HashMap<Edge, number>(hashEdge, equalEdges); |
| 26 | + |
| 27 | +// Set values for keys |
| 28 | +map.set({ from: 'A', to: 'B' }, 10); |
| 29 | +map.set({ from: 'C', to: 'D' }, 20); |
| 30 | + |
| 31 | +// Update value for an equal key with a different object identity |
| 32 | +map.set({ from: 'A', to: 'B' }, 100); |
| 33 | + |
| 34 | +// Get an existing value by the equal key with a different identity |
| 35 | +const value = map.get({ from: 'A', to: 'B' }); |
| 36 | + |
| 37 | +// Iterate over entries in the original insertion order |
| 38 | +for (const [key, value] of map) { |
| 39 | + console.log('Map entry: ', key, value); |
| 40 | +} |
| 41 | + |
| 42 | +// Clone the map with the same hash and equality functions |
| 43 | +const clonedMap = map.clone(); |
| 44 | + |
| 45 | +// Delete entry for an equal key with a different object identity |
| 46 | +map.delete({ from: 'A', to: 'B' }); |
| 47 | + |
| 48 | +// Clear the map |
| 49 | +map.clear(); |
| 50 | +``` |
| 51 | + |
| 52 | +## API |
| 53 | + |
| 54 | +The library has the following exports: |
| 55 | + |
| 56 | +#### `HashMap<K, V>` class |
| 57 | + |
| 58 | +Fully compatible with built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with a different constructor and an additional method. |
| 59 | + |
| 60 | +The `HashMap` keeps the entries in the original insertion order the same way as the built-in `Map` does. |
| 61 | + |
| 62 | +##### `HashMap<K, V>(hash, equals, entries?)` constructor |
| 63 | + |
| 64 | +Constructs a new hash map with the specified hash and equality functions for keys. |
| 65 | + |
| 66 | +Parameters: |
| 67 | +* `hash: (k: Key) => number` |
| 68 | +* `equals: (k1, k2) => boolean` |
| 69 | +* `entries?: Iterable<readonly [K, V]>` |
| 70 | + |
| 71 | +##### `HashMap<K, V>.clone(): HashMap<K, V>` method |
| 72 | + |
| 73 | +Returns a copy of the map with the same entries, hash and equality functions for the keys. |
| 74 | + |
| 75 | +#### `HashSet<T>` class |
| 76 | + |
| 77 | +Fully compatible with built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) with a different constructor and an additional method. |
| 78 | + |
| 79 | +The `HashSet` keeps the entries in the original insertion order the same way as the built-in `Set` does. |
| 80 | + |
| 81 | +##### `HashSet<T>(hash, equals, items?)` constructor |
| 82 | + |
| 83 | +Constructs a new hash set with the specified hash and equality functions for items. |
| 84 | + |
| 85 | +Parameters: |
| 86 | +* `hash: (v: T) => number` |
| 87 | +* `equals: (v1, v2) => boolean` |
| 88 | +* `items?: Iterable<T>` |
| 89 | + |
| 90 | +##### `HashSet<T>.clone(): HashMap<T>` method |
| 91 | + |
| 92 | +Returns a copy of the set with the same items, hash and equality functions for the items. |
| 93 | + |
| 94 | +#### `ReadonlyHashMap<K, V>` interface |
| 95 | + |
| 96 | +TypeScript interface for a read-only version of a `HashMap` which is compatible with `ReadonlyMap`. |
| 97 | + |
| 98 | +#### `ReadonlyHashSet<T>` interface |
| 99 | + |
| 100 | +TypeScript interface for a read-only version of a `HashSet` which is compatible with `ReadonlySet`. |
| 101 | + |
| 102 | +#### `hashString(value: string): number` function |
| 103 | + |
| 104 | +Utility function to compute a string hash using [FNV-32a](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) algorithm. |
| 105 | + |
| 106 | +#### `hashNumber(value: number): number` function |
| 107 | + |
| 108 | +Utility function to compute a hash for a JS number: |
| 109 | +* for 32-bit integers the hash is the number itself; |
| 110 | +* otherwise the hash is `Math.imul(31, H) + L` where `H` and `L` are 32-bit parts of IEEE floating point representation of the number. |
| 111 | + |
| 112 | +#### `hashBigInt(value: bigint): number` function |
| 113 | + |
| 114 | +Utility function to compute a hash for a `bigint` value with the following formula: `abs(N) % 0x1_0000_0000` where N is the `bigint` value. |
| 115 | + |
| 116 | +#### `hashValue(value: string | number | bigint | boolean | undefined | null)` |
| 117 | + |
| 118 | +Utility function to compute a hash for a primitive JS value: |
| 119 | +* for `string` the `hashString(value)` is used; |
| 120 | +* for `number` the `hashNumber(value)` is used; |
| 121 | +* for `bigint` the `hashBigInt(value)` is used; |
| 122 | +* for `null` the hash is `0`; |
| 123 | +* for `undefined` the hash is `1`; |
| 124 | +* for `boolean` the hash is `3` for `false` and `4` for `true`; |
| 125 | +* otherwise the hash is `0`. |
| 126 | + |
| 127 | +#### `chainHash(hash: number, added: number): number` |
| 128 | + |
| 129 | +Utility function to "chain" hash values when computing a hash of a composite value, for example: |
| 130 | + |
| 131 | +```ts |
| 132 | +let hash = 0; |
| 133 | +hash = chainHash(hash, hashValue(key.fieldA)); |
| 134 | +hash = chainHash(hash, hashValue(key.fieldB)); |
| 135 | +hash = chainHash(hash, hashValue(key.fieldC)); |
| 136 | +``` |
| 137 | + |
| 138 | +The formula for chained hash is `(Math.imul(hash, 31) + added) | 0`. |
| 139 | + |
| 140 | +#### `hashTuple(...values): number` |
| 141 | + |
| 142 | +Utility function to compute hash for a tuple of primitive values, equivalent to subsequent calls to `chainHash()` with `hashValue()` for each argument with the initial hash value of a hashed tuple length: |
| 143 | + |
| 144 | +```ts |
| 145 | +const hash = hashTuple(key.fieldA, key.fieldB, key.fieldC); |
| 146 | +// same as |
| 147 | +let hash = hashNumber(3); |
| 148 | +hash = chainHash(hash, hashValue(key.fieldA)); |
| 149 | +hash = chainHash(hash, hashValue(key.fieldB)); |
| 150 | +hash = chainHash(hash, hashValue(key.fieldC)); |
| 151 | +``` |
| 152 | + |
| 153 | +#### `dropHighestNonSignBit(hash: number): number` |
| 154 | + |
| 155 | +Utility function which coerces a number value into an integer such that "small integer optimization" ([Smi](https://v8.dev/blog/pointer-compression)) in V8 engine can be applied. |
| 156 | + |
| 157 | +Passing the value from this function to return a computed hash may improve performance in some specific situations. |
| 158 | + |
| 159 | +## License |
| 160 | + |
| 161 | +The library is distributed under MIT license, see [LICENSE](./LICENSE). |
0 commit comments