├── .gitignore ├── .istanbul.yml ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── lib └── multikeymap.js ├── package.json └── tests └── unit └── multikeymap.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # IDE user settings 30 | .idea 31 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: . 4 | extensions: 5 | - .js 6 | default-excludes: true 7 | excludes: [] 8 | variable: __coverage__ 9 | compact: true 10 | preserve-comments: false 11 | complete-copy: false 12 | save-baseline: false 13 | baseline-file: ./coverage/coverage-baseline.raw.json 14 | include-all-sources: false 15 | include-pid: false 16 | es-modules: false 17 | auto-wrap: false 18 | reporting: 19 | print: summary 20 | reports: 21 | - lcov 22 | dir: ./coverage 23 | summarizer: pkg 24 | report-config: {} 25 | watermarks: 26 | statements: [50, 80] 27 | functions: [50, 80] 28 | branches: [50, 80] 29 | lines: [50, 80] 30 | hooks: 31 | hook-run-in-context: false 32 | post-require-hook: null 33 | handle-sigint: false 34 | check: 35 | global: 36 | statements: 0 37 | lines: 0 38 | branches: 0 39 | functions: 0 40 | excludes: [] 41 | each: 42 | statements: 100 43 | lines: 100 44 | branches: 96 45 | functions: 100 46 | excludes: [] 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | .git 3 | .gitignore 4 | .idea 5 | .npmrc 6 | .travis.yml 7 | .istanbul.yml 8 | coverage 9 | node_modules 10 | NOTES.md 11 | CONTRIBUTING.md 12 | CHANGELONG.md 13 | npm-debug.log 14 | tests 15 | docs 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.0 4 | 5 | ### 1.0.0 6 | * Initial release. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute: 4 | 5 | * Create a feature or bugfix branch of master. 6 | * Clone your branch. 7 | * Run `$ node install` from the project's root folder. 8 | 9 | Contributions are welcome as long provided the follow the guidelines outlined below. Failure to satisfy all guidelines will result in rejection. 10 | 11 | * All modifications must be made via pull request. 12 | 13 | * All updates must update the version number according to the [semver](http://semver.org/) guideline. 14 | 15 | * All updates must include a description of the changes in the [Change Log](CHANGELOG.md). 16 | 17 | * All changes to the core module code must include proper test coverage. Tests must reach 100% code coverage. 18 | 19 | * All commits to master must result in a successful build and test run. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiKeyMap 2 | 3 | A `MultiKeyMap` functions much like a hash table, but it allows you to map more than one key to a single value. In this scenario, an entry is keyed by an array of values. The `MultiKeyMap` class does not place requirements on the order of values to set and get values. 4 | 5 | ## Usage 6 | 7 | Add the `multikeymap` module to your project: 8 | 9 | ```sh 10 | $ npm install multikeymap -S 11 | ``` 12 | 13 | Then create instances of the `MultiKeyMap` class: 14 | 15 | ```js 16 | const MultiKeyMap = require('multikeymap'); 17 | 18 | const map = new MultiKeyMap(); 19 | 20 | map.set(['a', 'b', 'c'], '123'); 21 | 22 | console.log( map.get(['a', 'b', 'c']) ); // 123 23 | console.log( map.get(['b', 'a', 'c']) ); // 123 24 | console.log( map.get(['c', 'a', 'b']) ); // 123 25 | console.log( map.get(['b', 'a']) ); // undefined 26 | ``` 27 | 28 | ## API 29 | 30 | ### Class: `MultiKeyMap` 31 | 32 | The interface for the `MultiKeyMap` class looks very similar to the native ECMAScript `Map` object. 33 | 34 | `MultiKeyMap` instances store keys and values in a directed graph structure where each vertex is a key used in a keys array, and edges point to all associated key component. This allows for lookups of values that do not depend on key array order. 35 | 36 | * __Properties__ 37 | 38 | + `MultiKeyMap.prototype.size`: the number of entries in the `MultiKeyMap` instance. 39 | 40 | + `MultiKeyMap.prototype[Symbol.toStringTag]`: the tag to use when stringifying the `MultiKeyMap` instance. 41 | 42 | + `MultiKeyMap[Symbol.species]`: a reference to the `MultiKeyMap` constructor. 43 | 44 | * __Methods__ 45 | 46 | + `MultiKeyMap.prototype.clear()`: clears the contents of the `MultiKeyMap` instance. 47 | 48 | + `MultiKeyMap.prototype.delete(keys)`: deletes the entry associated with the given array of keys. Returns a `Boolean` value indicating whether or not an item was actually deleted. Parameters: 49 | 50 | - `keys`: _(required)_ an array of keys referencing the entry to delete. 51 | 52 | + `MultiKeyMap.prototype.entries()`: returns an ECMAScript [`Iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) object that contains the key array and value pairs for each entry in the `MultiKeyMap` instance. 53 | 54 | + `MultiKeyMap.prototype.forEach(callback [, thisArg])`: executes the provided function once for each keys array and value entry in the `MultiKeyMap` instance. Parameters: 55 | 56 | - `callback`: _(required)_ the function to call for each entry in the `MultiKeyMap` instance. This function is called with the parameters: 57 | 58 | - `keys`: the array of keys used to reference the entry. 59 | 60 | - `value`: the value for the entry. 61 | 62 | - `map`: a reference to the `MultiKeyMap` instance. 63 | 64 | - `thisArg`: _(optional)_ value to use as `this` when executing `callback`. 65 | 66 | + `MultiKeyMap.prototype.get(keys)`: returns the value for the given array of keys. If no entry is found `undefined` is returned. Parameters: 67 | 68 | - `keys`: _(required)_ an array of keys referencing the entry to lookup. 69 | 70 | + `MultiKeyMap.prototype.has(keys)`: returns a `Boolean` indicating whether or not the `MultiKeyMap` instance contains an entry for the given array of keys. 71 | 72 | - `keys`: _(required)_ an array of keys referencing the entry to lookup. 73 | 74 | + `MultiKeyMap.prototype.keys()`: returns an ECMAScript `Iterator` object that contains the key arrays for each entry in the `MultiKeyMap` instance. 75 | 76 | + `MultiKeyMap.prototype.set(keys, value)`: sets a value for the given keys. If there is already a value for the given `keys` it is overwritten with the new value. Parameters: 77 | 78 | - `keys`: _(required)_ an array of keys to reference the value. 79 | 80 | - `value`: _(required)_ the value of the entry. 81 | 82 | + `MultiKeyMap.prototype.traverse()`: returns a [`Traversor`](#class-traversor) object used to walk through the `MultiKeyMap` instance's internal graph to locate a specific value. 83 | 84 | + `MultiKeyMap.prototype.values()`: returns an ECMAScript `Iterator` object that contains the values for each entry in the `MultiKeyMap` instance. 85 | 86 | + `MultiKeyMap.prototype[Symbol.iterator]()`: returns the same ECMAScript `Iterator` object as `MultiKeyMap.prototype.entries()`. 87 | 88 | ### Class: `Traversor` 89 | 90 | An object used to traverse through a `MultiKeyMap` instance's internal graph structure one key component at a time. This object functions similarly to an ECMAScript `Iterator`. Advancing a `Traversor` is similar to a reducer function, in that all previously requested key values limit the next possible node the `Traversor` can visit. 91 | 92 | Instances of `Traversor` are created by calling `MultiKeyMap.prototype.traverse()` 93 | 94 | * __Methods__ 95 | 96 | + `Traversor.prototype.next(key)`: advances the `Traversor` instance to the next specified key. A call to `next()` amounts to hash lookup, and is performed in _O(1)_ constant time. Parameters: 97 | 98 | - `key`: the next key component to move to in the connected `MultiKeyMap`'s internal graph. 99 | 100 | The `next()` method returns an object with the following keys: 101 | 102 | - `done`: a `Boolean` value indicating whether or not there is a connected vertex with the given `key` name. If this value is `true`, the `Traversor` has essentially hit a dead end, and cannot be advanced any further. 103 | 104 | - `value`: the value at the current vertex. If no value exists at the current vertext, then this is set to `undefined`. This key is omitted if `done` is `true`. 105 | 106 | * __Example__ 107 | 108 | ```js 109 | const MultiKeyMap = require('multikeymap'); 110 | 111 | const map = new MultiKeyMap(); 112 | 113 | map.set(['a', 'b', 'c'], 'foo'); 114 | map.set(['a', 'c'], 'bar'); 115 | map.set(['a', 'b', 'c', 'd'], 'baz'); 116 | 117 | const traverse = map.traverse(); 118 | 119 | const b = traverse.next('b'); 120 | console.log(b); // { done: false, value: undefined } 121 | 122 | const a = traverse.next('a'); 123 | console.log(a); // { done: false, value: undefined } 124 | 125 | const c = traverse.next('c'); 126 | console.log(c); // { done: false, value: 'foo' } 127 | 128 | const d = traverse.next('d'); 129 | console.log(d); // { done: false, value: 'baz' } 130 | 131 | const e = traverse.next('e'); 132 | console.log(e); // { done: true } 133 | ``` 134 | 135 | ## Performance 136 | 137 | The `MultiKeyMap` class is read-optimized. All lookups are done in _O(n)_ linear time, where _n_ is the number of items in a keys array. 138 | 139 | ## Compatibility 140 | 141 | This library is written using ECMAScript 2015 (version 6), and consequently may not be compatible with older browsers. Specifically, this module utilizes `class`, `Map`, `Set`, and `Symbol`. Refer to the [ECMAScript compatibility chart](https://kangax.github.io/compat-table/es6/) to see if the `multikeymap` module will work in your target browsers. 142 | -------------------------------------------------------------------------------- /lib/multikeymap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Iterator { 4 | 5 | constructor(map, justValue) { 6 | this._map = map; 7 | this._current = -1; 8 | this._justValue = justValue; 9 | } 10 | 11 | 12 | next() { 13 | this._current++; 14 | 15 | if (this._current >= this._map._keys.length) 16 | return { done: true }; 17 | 18 | const keys = this._map._keys[this._current]; 19 | const value = this._map.get(keys); 20 | 21 | if (this._justValue) return { done: false, value: value }; 22 | 23 | return { done: false, value: [ keys, value ] }; 24 | } 25 | 26 | } 27 | 28 | 29 | class Traversor { 30 | 31 | constructor(node) { 32 | this._node = node; 33 | this._done = false; 34 | this._current = undefined; 35 | } 36 | 37 | 38 | next(key) { 39 | if (this._done) return this._current; 40 | 41 | this._node = this._node.neighbors.get(key); 42 | 43 | if (typeof this._node === 'undefined') { 44 | this._done = true; 45 | this._current = { done: true }; 46 | return this._current; 47 | } 48 | 49 | const value = (typeof this._node.data === 'undefined') 50 | ? undefined 51 | : this._node.data.value; 52 | 53 | this._current = { done: false, value: value }; 54 | 55 | return this._current; 56 | } 57 | 58 | } 59 | 60 | 61 | class MultiKeyMap { 62 | 63 | constructor() { 64 | this._keys = []; 65 | this._root = { neighbors: new Map() }; 66 | } 67 | 68 | 69 | get size() { return this._keys.length; } 70 | 71 | 72 | get [Symbol.toStringTag]() { return 'MultiKeyMap'; } 73 | 74 | 75 | static get [Symbol.species]() { return MultiKeyMap; } 76 | 77 | 78 | _assertKeys(keys) { 79 | if (!Array.isArray(keys)) 80 | throw new TypeError('Argument "keys" must be an array'); 81 | } 82 | 83 | 84 | _assertCallback(callback) { 85 | if (typeof callback !== 'function') 86 | throw new TypeError('Argument "calback" must be a function'); 87 | } 88 | 89 | 90 | clear() { 91 | this._root.neighbors.clear(); 92 | this._keys = []; 93 | } 94 | 95 | 96 | delete(keys) { 97 | const result = this._delete(keys, this._root); 98 | if (result.deleted) { 99 | this._deleteKeys(result.keys); 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | 107 | _delete(keys, node) { 108 | this._assertKeys(keys); 109 | 110 | if (keys.length === 0) return false; 111 | 112 | let result = { 113 | deleted: false, 114 | keys: undefined, 115 | }; 116 | 117 | for (let i = 0; i < keys.length; i++) { 118 | const key = keys[i]; 119 | let nextNode = node.neighbors.get(key); 120 | 121 | if (typeof nextNode === 'undefined') continue; 122 | 123 | if (keys.length === 1) { 124 | if (typeof nextNode.data !== 'undefined') { 125 | result.deleted = true; 126 | result.keys = nextNode.data.keys; 127 | nextNode.data = undefined; 128 | } 129 | } else { 130 | const copy = keys.slice(); 131 | copy.splice(i, 1); 132 | const res = this._delete(copy, nextNode); 133 | if (res.deleted) result = res; 134 | } 135 | 136 | if (nextNode.neighbors.size === 0) 137 | node.neighbors.delete(key); 138 | } 139 | 140 | return result; 141 | } 142 | 143 | 144 | _deleteKeys(keys) { 145 | for (let i = 0; i < this._keys.length; i++) { 146 | if (this._keys[i] === keys) { 147 | this._keys.splice(i, 1); 148 | return; 149 | } 150 | } 151 | } 152 | 153 | 154 | entries() { 155 | return new Iterator(this, false); 156 | } 157 | 158 | 159 | forEach(callback, self) { 160 | this._assertCallback(callback); 161 | 162 | const keys = this._keys; 163 | const setSelf = (typeof self !== 'undefined' && self !== null); 164 | 165 | for (let i = 0; i < keys.length; i++) { 166 | const key = keys[i]; 167 | const value = this.get(key); 168 | if (setSelf) callback.call(self, key, value, this) 169 | else callback(key, value, this); 170 | } 171 | } 172 | 173 | 174 | get(keys) { 175 | const result = this._get(keys); 176 | if (typeof result === 'undefined') return undefined; 177 | return result.value; 178 | } 179 | 180 | 181 | _get(keys) { 182 | this._assertKeys(keys); 183 | if (keys.length === 0) return undefined; 184 | 185 | let node = this._root; 186 | 187 | for (let i = 0; i < keys.length; i++) { 188 | const key = keys[i]; 189 | node = node.neighbors.get(key); 190 | if (typeof node === 'undefined') return undefined; 191 | } 192 | 193 | return node.data; 194 | } 195 | 196 | 197 | has(keys) { 198 | return (typeof this._get(keys) !== 'undefined'); 199 | } 200 | 201 | 202 | keys() { 203 | return this._keys[Symbol.iterator](); 204 | } 205 | 206 | 207 | set(keys, value) { 208 | this._assertKeys(keys); 209 | 210 | if (arguments.length < 2) 211 | throw new TypeError('The "value" argument is required'); 212 | 213 | const added = this._set(keys, keys, value, this._root); 214 | 215 | if (added) this._keys.push(keys); 216 | 217 | return this; 218 | } 219 | 220 | 221 | _set(base, keys, value, node) { 222 | let added = false; 223 | 224 | for (let i = 0; i < keys.length; i++) { 225 | const key = keys[i]; 226 | let nextNode = node.neighbors.get(key); 227 | 228 | if (typeof nextNode === 'undefined') { 229 | nextNode = { 230 | data: undefined, 231 | neighbors: new Map(), 232 | }; 233 | 234 | node.neighbors.set(key, nextNode); 235 | } 236 | 237 | if (keys.length === 1) { 238 | added = (typeof nextNode.data === 'undefined'); 239 | nextNode.data = { 240 | value: value, 241 | keys: base, 242 | }; 243 | return added; 244 | } 245 | 246 | const copy = keys.slice(); 247 | copy.splice(i, 1); 248 | 249 | added = this._set(base, copy, value, nextNode); 250 | } 251 | 252 | return added; 253 | } 254 | 255 | 256 | traverse() { 257 | return new Traversor(this._root); 258 | } 259 | 260 | 261 | values() { 262 | return new Iterator(this, true); 263 | } 264 | 265 | 266 | [Symbol.iterator]() { 267 | return this.entries(); 268 | } 269 | 270 | } 271 | 272 | module.exports = MultiKeyMap; 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multikeymap", 3 | "longName": "MultiKeyMap", 4 | "description": "A multi-key hash table for JavaScript.", 5 | "version": "1.0.0", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "chai": "~3.5.0", 9 | "istanbul": "~0.4.2", 10 | "mocha": "~2.4.5" 11 | }, 12 | "keywords": [ 13 | "multi", 14 | "key", 15 | "vector", 16 | "map", 17 | "hashtable" 18 | ], 19 | "main": "./lib/multikeymap.js", 20 | "private": false, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/dsfields/multikeymap-js" 24 | }, 25 | "scripts": { 26 | "lint": "eslint ./**/*.js", 27 | "test": "NODE_ENV=test istanbul cover _mocha tests/unit/**/*.js" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/unit/multikeymap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | 5 | const MultiKeyMap = require('../../lib/multikeymap'); 6 | 7 | describe('MultiKeyMap', () => { 8 | 9 | const key = ['a', 'b', 'c']; 10 | const value = 'foo'; 11 | let map; 12 | 13 | beforeEach(() => { 14 | map = new MultiKeyMap(); 15 | map.set(key, value); 16 | }); 17 | 18 | describe('#size', () => { 19 | it('should be 0 one construction', () => { 20 | const m = new MultiKeyMap(); 21 | assert.strictEqual(m.size, 0); 22 | }); 23 | 24 | it('should increment after set', () => { 25 | assert.strictEqual(map.size, 1); 26 | }); 27 | 28 | it('should decrement after delete', () => { 29 | map.delete(key); 30 | assert.strictEqual(map.size, 0); 31 | }); 32 | }); 33 | 34 | describe('#[Symbol.toStringTag]', () => { 35 | it('should be string', () => { 36 | assert.isString(map[Symbol.toStringTag]); 37 | }); 38 | }); 39 | 40 | describe('#[Symbol.species]', () => { 41 | it('should return MultiKeyMap constructor', () => { 42 | assert.strictEqual(MultiKeyMap[Symbol.species], MultiKeyMap); 43 | }); 44 | 45 | it('should be constructable as MultiKeyMap', () => { 46 | const Species = MultiKeyMap[Symbol.species]; 47 | const species = new Species(); 48 | assert.instanceOf(species, MultiKeyMap); 49 | }); 50 | }); 51 | 52 | describe('#clear', () => { 53 | it('should set size to 0', () => { 54 | map.clear(); 55 | assert.strictEqual(map.size, 0); 56 | }); 57 | 58 | it('should make all entries ungettable', () => { 59 | map.clear(); 60 | const result = map.get(key); 61 | assert.isNotOk(result); 62 | }); 63 | }); 64 | 65 | describe('#delete', () => { 66 | it('should throw if keys not array', () => { 67 | assert.throws(() => { 68 | map.delete(42); 69 | }, TypeError); 70 | }); 71 | 72 | it('should result in item not being gettable', () => { 73 | map.delete(key); 74 | const result = map.get(key); 75 | assert.isNotOk(result); 76 | }); 77 | 78 | it('should decrement size', () => { 79 | map.delete(key); 80 | assert.strictEqual(map.size, 0); 81 | }); 82 | 83 | it('should return true if removed', () => { 84 | const result = map.delete(key); 85 | assert.isTrue(result); 86 | }); 87 | 88 | it('should return false if not removed', () => { 89 | const result = map.delete(['nope']); 90 | assert.isFalse(result); 91 | }); 92 | 93 | it('should return false if keys length is 0', () => { 94 | const result = map.delete([]); 95 | assert.isFalse(result); 96 | }); 97 | }); 98 | 99 | describe('#entries', () => { 100 | it('should return iterator', () => { 101 | const iterator = map.entries(); 102 | assert.isFunction(iterator.next); 103 | }); 104 | 105 | it('should return object on next', () => { 106 | const iterator = map.entries(); 107 | const item = iterator.next(); 108 | assert.isObject(item); 109 | }); 110 | 111 | it('should return object with done=false on first iteration', () => { 112 | const iterator = map.entries(); 113 | const item = iterator.next(); 114 | assert.isFalse(item.done); 115 | }); 116 | 117 | it('should return object with done=true on last iteration', () => { 118 | const iterator = map.entries(); 119 | iterator.next(); 120 | const item = iterator.next(); 121 | assert.isTrue(item.done); 122 | }); 123 | 124 | it('should return object with value on iteration', () => { 125 | const iterator = map.entries(); 126 | const item = iterator.next(); 127 | assert.property(item, 'value'); 128 | }); 129 | 130 | it('should return object value of array on iteration', () => { 131 | const iterator = map.entries(); 132 | const item = iterator.next(); 133 | assert.isArray(item.value); 134 | }); 135 | 136 | it('should return object value of array[0]=key on iteration', () => { 137 | const iterator = map.entries(); 138 | const item = iterator.next(); 139 | assert.strictEqual(item.value[0], key); 140 | }); 141 | 142 | it('should return object value of array[1]=value on iteration', () => { 143 | const iterator = map.entries(); 144 | const item = iterator.next(); 145 | assert.strictEqual(item.value[1], value); 146 | }); 147 | }); 148 | 149 | describe('#forEach', () => { 150 | it('should throw if callback not function', () => { 151 | assert.throws(() => { 152 | map.forEach(42); 153 | }, TypeError); 154 | }); 155 | 156 | it('should execute callback for each entry', () => { 157 | let count = 0; 158 | map.forEach(() => count++); 159 | assert.strictEqual(count, 1); 160 | }); 161 | 162 | it('should execute callback with key as first argument', () => { 163 | map.forEach((keys, val, m) => { 164 | assert.strictEqual(keys, key); 165 | }); 166 | }); 167 | 168 | it('should execute callback with value as second argument', () => { 169 | map.forEach((keys, val, m) => { 170 | assert.strictEqual(val, value); 171 | }); 172 | }); 173 | 174 | it('should execute callback with instance as third argument', () => { 175 | map.forEach((keys, val, m) => { 176 | assert.strictEqual(m, map); 177 | }); 178 | }); 179 | 180 | it('should execute callback with this context when provided', () => { 181 | const self = { a: 'b' }; 182 | map.forEach(function(keys, val, m) { 183 | assert.strictEqual(this, self); 184 | }, self); 185 | }); 186 | }); 187 | 188 | describe('#get', () => { 189 | it('should throw if keys not array', () => { 190 | assert.throws(() => { 191 | map.get(42); 192 | }, TypeError); 193 | }); 194 | 195 | it('should return value when found', () => { 196 | const result = map.get(key); 197 | assert.strictEqual(result, value); 198 | }); 199 | 200 | it('should return undefined when not found', () => { 201 | const result = map.get(['blorg']); 202 | assert.isNotOk(result); 203 | }); 204 | 205 | it('should return undefined with empty map', () => { 206 | const m = new MultiKeyMap(); 207 | const result = m.get(key); 208 | assert.isNotOk(result); 209 | }); 210 | 211 | it('should return undefined if keys length is 0', () => { 212 | const result = map.get([]); 213 | assert.isNotOk(result); 214 | }); 215 | }); 216 | 217 | describe('#has', () => { 218 | it('should throw if keys not array', () => { 219 | assert.throws(() => { 220 | map.has(42); 221 | }, TypeError); 222 | }); 223 | 224 | it('should return true if key exists', () => { 225 | const result = map.has(key); 226 | assert.isTrue(result); 227 | }); 228 | 229 | it('should return false if key does not exist', () => { 230 | const result = map.has(['nope']); 231 | assert.isFalse(result); 232 | }); 233 | }); 234 | 235 | describe('#keys', () => { 236 | it('should return iterator', () => { 237 | const iterator = map.keys(); 238 | assert.isFunction(iterator.next); 239 | }); 240 | 241 | it('should return object on next', () => { 242 | const iterator = map.keys(); 243 | const item = iterator.next(); 244 | assert.isObject(item); 245 | }); 246 | 247 | it('should return object with done=false on first iteration', () => { 248 | const iterator = map.keys(); 249 | const item = iterator.next(); 250 | assert.isFalse(item.done); 251 | }); 252 | 253 | it('should return object with done=true on last iteration', () => { 254 | const iterator = map.keys(); 255 | iterator.next(); 256 | const item = iterator.next(); 257 | assert.isTrue(item.done); 258 | }); 259 | 260 | it('should return object with value on iteration', () => { 261 | const iterator = map.keys(); 262 | const item = iterator.next(); 263 | assert.property(item, 'value'); 264 | }); 265 | 266 | it('should return object value of array on iteration', () => { 267 | const iterator = map.keys(); 268 | const item = iterator.next(); 269 | assert.isArray(item.value); 270 | }); 271 | 272 | it('should return object value of key on iteration', () => { 273 | const iterator = map.keys(); 274 | const item = iterator.next(); 275 | assert.strictEqual(item.value, key); 276 | }); 277 | 278 | it('should iterate with value as array', () => { 279 | const iterator = map.keys(); 280 | 281 | for (let value of iterator) { 282 | assert.isArray(value); 283 | return; 284 | } 285 | }); 286 | 287 | it('should iterate with value of key', () => { 288 | const iterator = map.keys(); 289 | 290 | for (let value of iterator) { 291 | assert.strictEqual(value, key); 292 | return; 293 | } 294 | }); 295 | 296 | it('should iterate once for each entry', () => { 297 | const iterator = map.keys(); 298 | let count = 0; 299 | 300 | for (let value of iterator) { 301 | count++; 302 | } 303 | 304 | assert.strictEqual(count, 1); 305 | }); 306 | }); 307 | 308 | describe('#set', () => { 309 | it('should throw if keys not array', () => { 310 | assert.throws(() => { 311 | map.set(42, 'hi'); 312 | }, TypeError); 313 | }); 314 | 315 | it('should throw if no value argument', () => { 316 | assert.throws(() => { 317 | map.set([42]); 318 | }, TypeError); 319 | }); 320 | 321 | it('should add item when it does not exist', () => { 322 | const k = ['x', 'y']; 323 | const v = 42; 324 | map.set(k, v); 325 | const result = map.get(k); 326 | assert.strictEqual(result, v); 327 | }); 328 | 329 | it('should update item if it exists', () => { 330 | const v = 'bar'; 331 | map.set(key, v); 332 | const result = map.get(key); 333 | assert.strictEqual(result, v); 334 | }); 335 | 336 | it('should increment size if item does not exist', () => { 337 | const current = map.size; 338 | map.set(['x', 'y'], 42); 339 | assert.strictEqual(map.size, current + 1); 340 | }); 341 | 342 | it('should not increment size if item exists', () => { 343 | const current = map.size; 344 | map.set(key, 42); 345 | assert.strictEqual(map.size, current); 346 | }); 347 | }); 348 | 349 | describe('#traverse', () => { 350 | it('should return traversor', () => { 351 | const traversor = map.traverse(); 352 | assert.isFunction(traversor.next); 353 | }); 354 | 355 | it('should return object on next', () => { 356 | const traversor = map.traverse(); 357 | const result = traversor.next(key[0]); 358 | assert.isObject(result); 359 | }); 360 | 361 | it('should return object with done=false on valid next', () => { 362 | const traversor = map.traverse(); 363 | const result = traversor.next(key[0]); 364 | assert.isFalse(result.done); 365 | }); 366 | 367 | it('should return object with value on valid next', () => { 368 | const traversor = map.traverse(); 369 | const result = traversor.next(key[0]); 370 | assert.property(result, 'value'); 371 | }); 372 | 373 | it('should return object with value=value on next with value', () => { 374 | const traversor = map.traverse(); 375 | traversor.next(key[0]); 376 | traversor.next(key[1]); 377 | const result = traversor.next(key[2]); 378 | assert.strictEqual(result.value, value); 379 | }); 380 | 381 | it('should return object with done=true on next after leaf', () => { 382 | const traversor = map.traverse(); 383 | traversor.next(key[0]); 384 | traversor.next(key[1]); 385 | traversor.next(key[2]); 386 | const result = traversor.next('blorg'); 387 | assert.isTrue(result.done); 388 | }); 389 | 390 | it('should return object with done=true on invalid key on next', () => { 391 | const traversor = map.traverse(); 392 | const result = traversor.next('blorg'); 393 | assert.isTrue(result.done); 394 | }); 395 | 396 | it('should return object with value=undefined on bridge next', () => { 397 | const traversor = map.traverse(); 398 | const result = traversor.next(key[0]); 399 | assert.isNotOk(result.value); 400 | }); 401 | 402 | it('should return object with done=true on successive invalid next', () => { 403 | const traversor = map.traverse(); 404 | traversor.next(key[0]); 405 | traversor.next(key[1]); 406 | traversor.next(key[2]); 407 | traversor.next('blorg'); 408 | const result = traversor.next('ugh'); 409 | assert.isTrue(result.done); 410 | }); 411 | }); 412 | 413 | describe('#values', () => { 414 | it('should return iterator', () => { 415 | const iterator = map.values(); 416 | assert.isFunction(iterator.next); 417 | }); 418 | 419 | it('should return object on next', () => { 420 | const iterator = map.values(); 421 | const item = iterator.next(); 422 | assert.isObject(item); 423 | }); 424 | 425 | it('should return object with done=false on first iteration', () => { 426 | const iterator = map.values(); 427 | const item = iterator.next(); 428 | assert.isFalse(item.done); 429 | }); 430 | 431 | it('should return object with done=true on last iteration', () => { 432 | const iterator = map.values(); 433 | iterator.next(); 434 | const item = iterator.next(); 435 | assert.isTrue(item.done); 436 | }); 437 | 438 | it('should return object with value on iteration', () => { 439 | const iterator = map.values(); 440 | const item = iterator.next(); 441 | assert.property(item, 'value'); 442 | }); 443 | 444 | it('should return object value of value on iteration', () => { 445 | const iterator = map.values(); 446 | const item = iterator.next(); 447 | assert.strictEqual(item.value, value); 448 | }); 449 | }); 450 | 451 | describe('#[Symbol.iterator]', () => { 452 | it('should return iterator', () => { 453 | const iterator = map[Symbol.iterator](); 454 | assert.isFunction(iterator.next); 455 | }); 456 | 457 | it('should return object on next', () => { 458 | const iterator = map[Symbol.iterator](); 459 | const item = iterator.next(); 460 | assert.isObject(item); 461 | }); 462 | 463 | it('should return object with done=false on first iteration', () => { 464 | const iterator = map[Symbol.iterator](); 465 | const item = iterator.next(); 466 | assert.isFalse(item.done); 467 | }); 468 | 469 | it('should return object with done=true on last iteration', () => { 470 | const iterator = map[Symbol.iterator](); 471 | iterator.next(); 472 | const item = iterator.next(); 473 | assert.isTrue(item.done); 474 | }); 475 | 476 | it('should return object with value on iteration', () => { 477 | const iterator = map[Symbol.iterator](); 478 | const item = iterator.next(); 479 | assert.property(item, 'value'); 480 | }); 481 | 482 | it('should return object value of array on iteration', () => { 483 | const iterator = map[Symbol.iterator](); 484 | const item = iterator.next(); 485 | assert.isArray(item.value); 486 | }); 487 | 488 | it('should return object value of array[0]=key on iteration', () => { 489 | const iterator = map[Symbol.iterator](); 490 | const item = iterator.next(); 491 | assert.strictEqual(item.value[0], key); 492 | }); 493 | 494 | it('should return object value of array[1]=value on iteration', () => { 495 | const iterator = map[Symbol.iterator](); 496 | const item = iterator.next(); 497 | assert.strictEqual(item.value[1], value); 498 | }); 499 | 500 | it('should iterate with value as array', () => { 501 | for (let val of map) { 502 | assert.isArray(val); 503 | return; 504 | } 505 | }); 506 | 507 | it('should iterate with value of array[0]=key', () => { 508 | for (let val of map) { 509 | assert.strictEqual(val[0], key); 510 | return; 511 | } 512 | }); 513 | 514 | it('should iterate with value of array[1]=value', () => { 515 | for (let val of map) { 516 | assert.strictEqual(val[1], value); 517 | return; 518 | } 519 | }); 520 | 521 | it('should iterate once for each entry', () => { 522 | let count = 0; 523 | 524 | for (let val of map) { 525 | count++; 526 | } 527 | 528 | assert.strictEqual(count, 1); 529 | }); 530 | }); 531 | 532 | }); 533 | --------------------------------------------------------------------------------