diff --git a/CHANGELOG.md b/CHANGELOG.md index c019b499..8c8e9d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.10.0 * Adding `Index`. +* Adding `MultiIndex`. * Adding `MultiMap`. * Adding `MultiSet`. * Adding `SymSpell`. diff --git a/README.md b/README.md index c00bb7dd..937124bb 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Full documentation for the library can be found [here](https://yomguithereal.git * [Heap](https://yomguithereal.github.io/mnemonist/heap) * [Index](https://yomguithereal.github.io/mnemonist/index-structure) * [Linked List](https://yomguithereal.github.io/mnemonist/linked-list) +* [MultiIndex](https://yomguithereal.github.io/mnemonist/multi-index) * [MultiMap](https://yomguithereal.github.io/mnemonist/multi-map) * [MultiSet](https://yomguithereal.github.io/mnemonist/multi-set) * [Queue](https://yomguithereal.github.io/mnemonist/queue) diff --git a/endpoint.js b/endpoint.js index 91068d4b..80760c7f 100644 --- a/endpoint.js +++ b/endpoint.js @@ -20,6 +20,7 @@ module.exports = { MaxHeap: Heap.MaxHeap, Index: require('./index.js'), LinkedList: require('./linked-list.js'), + MultiIndex: require('./multi-index.js'), MultiMap: require('./multi-map.js'), MultiSet: require('./multi-set.js'), Queue: require('./queue.js'), diff --git a/index.js b/index.js index 0ebdac52..8e2c3784 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ /** * Mnemonist Index - * ==================== + * ================ * * The Index is basically an abstract HashMap where given keys or items * are hashed by a function to produce a specific key so that one may diff --git a/multi-index.js b/multi-index.js new file mode 100644 index 00000000..194da1aa --- /dev/null +++ b/multi-index.js @@ -0,0 +1,193 @@ +/** + * Mnemonist MultiIndex + * ===================== + * + * Same as the index but relying on a MultiMap rather than a Map. + */ +var MultiMap = require('./multi-map.js'), + iterateOver = require('./utils/iterate.js'); + +var identity = function(x) { + return x; +}; + +/** + * MultiIndex. + * + * @constructor + * @param {array|function} descriptor - Hash functions descriptor. + * @param {function} Container - Container to use. + */ +function MultiIndex(descriptor, Container) { + this.items = new MultiMap(Container); + this.clear(); + + if (Array.isArray(descriptor)) { + this.writeHashFunction = descriptor[0]; + this.readHashFunction = descriptor[1]; + } + else { + this.writeHashFunction = descriptor; + this.readHashFunction = descriptor; + } + + if (!this.writeHashFunction) + this.writeHashFunction = identity; + if (!this.readHashFunction) + this.readHashFunction = identity; + + if (typeof this.writeHashFunction !== 'function') + throw new Error('mnemonist/MultiIndex.constructor: invalid hash function given.'); + + if (typeof this.readHashFunction !== 'function') + throw new Error('mnemonist/MultiIndex.constructor: invalid hash function given.'); +} + +/** + * Method used to clear the structure. + * + * @return {undefined} + */ +MultiIndex.prototype.clear = function() { + this.items.clear(); + + // Properties + this.size = 0; +}; + +/** + * Method used to add an item to the index. + * + * @param {any} item - Item to add. + * @return {MultiIndex} + */ +MultiIndex.prototype.add = function(item) { + var key = this.writeHashFunction(item); + + this.items.set(key, item); + this.size = this.items.size; + + return this; +}; + +/** + * Method used to set an item in the index using the given key. + * + * @param {any} key - Key to use. + * @param {any} item - Item to add. + * @return {MultiIndex} + */ +MultiIndex.prototype.set = function(key, item) { + key = this.writeHashFunction(key); + + this.items.set(key, item); + this.size = this.items.size; + + return this; +}; + +/** + * Method used to retrieve an item from the index. + * + * @param {any} key - Key to use. + * @return {MultiIndex} + */ +MultiIndex.prototype.get = function(key) { + key = this.readHashFunction(key); + + return this.items.get(key); +}; + +/** + * Method used to iterate over each of the index's values. + * + * @param {function} callback - Function to call for each item. + * @param {object} scope - Optional scope. + * @return {undefined} + */ +MultiIndex.prototype.forEach = function(callback, scope) { + scope = arguments.length > 1 ? scope : this; + + this.items.forEach(function(value) { + callback.call(scope, value, value); + }); +}; + + +/** + * MultiIndex Iterator class. + */ +function MultiIndexIterator(next) { + this.next = next; +} + +/** + * Method returning an iterator over the index's values. + * + * @return {MultiIndexIterator} + */ +MultiIndex.prototype.values = function() { + var iterator = this.items.values(); + + Object.defineProperty(iterator, 'constructor', { + value: MultiIndexIterator, + enumerable: false + }); + + return iterator; +}; + +/** + * Attaching the #.values method to Symbol.iterator if possible. + */ +if (typeof Symbol !== 'undefined') + MultiIndex.prototype[Symbol.iterator] = MultiIndex.prototype.values; + +/** + * Convenience known method. + */ +MultiIndex.prototype.inspect = function() { + var array = Array.from(this); + + Object.defineProperty(array, 'constructor', { + value: MultiIndex, + enumerable: false + }); + + return array; +}; + +/** + * Static @.from function taking an abitrary iterable & converting it into + * a structure. + * + * @param {Iterable} iterable - Target iterable. + * @param {array|function} descriptor - Hash functions descriptor. + * @param {function} Container - Container to use. + * @param {boolean} useSet - Whether to use #.set or #.add + * @return {MultiIndex} + */ +MultiIndex.from = function(iterable, descriptor, Container, useSet) { + if (arguments.length === 3) { + if (typeof Container === 'boolean') { + useSet = Container; + Container = Array; + } + } + + var index = new MultiIndex(descriptor, Container); + + iterateOver(iterable, function(value, key) { + if (useSet) + index.set(key, value); + else + index.add(value); + }); + + return index; +}; + +/** + * Exporting. + */ +module.exports = MultiIndex; diff --git a/test/multi-index.js b/test/multi-index.js new file mode 100644 index 00000000..a445133c --- /dev/null +++ b/test/multi-index.js @@ -0,0 +1,174 @@ +/* eslint no-new: 0 */ +/** + * Mnemonist MultiIndex Unit Tests + * =========================== + */ +var assert = require('assert'), + MultiIndex = require('../multi-index.js'); + +describe('MultiIndex', function() { + + it('should throw if given invalid hash functions.', function() { + + assert.throws(function() { + new MultiIndex({hello: 'world'}); + }, /hash/); + + assert.throws(function() { + new MultiIndex([{hello: 'world'}]); + }, /hash/); + + assert.throws(function() { + new MultiIndex([null, {hello: 'world'}]); + }, /hash/); + }); + + it('should be possible to add items to the index.', function() { + var index = new MultiIndex(function(item) { + return item.title.toLowerCase(); + }); + + index.add({title: 'Hello'}); + index.add({title: 'Hello'}); + index.add({title: 'World'}); + + assert.strictEqual(index.size, 3); + }); + + it('should be possible to set values.', function() { + var index = new MultiIndex(function(item) { + return item.toLowerCase(); + }); + + index.set('Hello', {title: 'Hello'}); + index.set('HeLLo', {title: 'Hello'}); + index.set('World', {title: 'World'}); + + assert.strictEqual(index.size, 3); + }); + + it('should be possible to clear the index.', function() { + var index = new MultiIndex(function(item) { + return item.title.toLowerCase(); + }); + + index.add({title: 'Hello'}); + index.add({title: 'World'}); + + index.clear(); + + assert.strictEqual(index.size, 0); + }); + + it('should be possible to get items from the index.', function() { + var index = new MultiIndex(function(item) { + return item.toLowerCase(); + }); + + index.set('Hello', {title: 'Hello1'}); + index.set('HellO', {title: 'Hello2'}); + index.set('World', {title: 'World'}); + + assert.deepEqual(index.get('HELLO'), [{title: 'Hello1'}, {title: 'Hello2'}]); + assert.deepEqual(index.get('shawarama'), undefined); + }); + + it('should be possible to iterate over an index\'s values.', function() { + var index = new MultiIndex(function(item) { + return item.toLowerCase(); + }); + + index.set('Hello', {title: 'Hello'}); + index.set('World', {title: 'World'}); + + var i = 0; + + index.forEach(function(value) { + assert.deepEqual(value, !i ? {title: 'Hello'} : {title: 'World'}); + i++; + }); + + assert.strictEqual(i, 2); + }); + + it('should be possible to create an iterator over an index\'s values.', function() { + var index = new MultiIndex(function(item) { + return item.toLowerCase(); + }); + + index.set('Hello', {title: 'Hello'}); + index.set('World', {title: 'World'}); + + var iterator = index.values(); + + assert.deepEqual(iterator.next().value, {title: 'Hello'}); + assert.deepEqual(iterator.next().value, {title: 'World'}); + assert.strictEqual(iterator.next().done, true); + }); + + it('should be possible to iterate over an index\'s values using for...of.', function() { + var index = new MultiIndex(function(item) { + return item.toLowerCase(); + }); + + index.set('Hello', {title: 'Hello'}); + index.set('World', {title: 'World'}); + + var i = 0; + + for (var value of index) { + assert.deepEqual(value, !i ? {title: 'Hello'} : {title: 'World'}); + i++; + } + + assert.strictEqual(i, 2); + }); + + it('should be possible to create an index from arbitrary iterables.', function() { + function writeHash(item) { + return item.title.toLowerCase(); + } + + function readHash(query) { + return query.toLowerCase(); + } + + var index = MultiIndex.from([{title: 'Hello'}, {title: 'World'}], [writeHash, readHash]); + + assert.strictEqual(index.size, 2); + assert.deepEqual(index.get('hellO'), [{title: 'Hello'}]); + + var map = new Map([ + ['Hello', {title: 'Hello'}], + ['World', {title: 'World'}] + ]); + + index = MultiIndex.from(map, readHash, true); + + assert.strictEqual(index.size, 2); + assert.deepEqual(index.get('WOrlD'), [{title: 'World'}]); + }); + + it('should work with a Set container.', function() { + var index = new MultiIndex(function(query) { + return query.toLowerCase(); + }, Set); + + var three = {title: 'Hello3'}; + + index.set('hello', {title: 'Hello1'}); + index.set('hellO', {title: 'Hello2'}); + index.set('HeLLo', three); + index.set('hello', three); + + assert.strictEqual(index.size, 3); + + var set = index.get('hello'); + + assert.deepEqual(Array.from(set), [ + {title: 'Hello1'}, + {title: 'Hello2'}, + {title: 'Hello3'} + ]); + }); +});