UNPKG

hashes

Version:

A package implementing a HashTable and HashSet

827 lines (694 loc) 31.6 kB
//A key-value pair constructor function for internal use var KeyValuePair = function (key, value) { this._key = key; this._value = value; }; ///Various static methods. Override these methods to change the behavior of all hashes objects var statics = (function () { return { //Makes sure that the key is valid for the HashTable verifyKey: function (key) { if (key === undefined || key === null) { throw new Error("Key cannot be undefined or null"); } return true; }, ///Converts a string to a numeric hash stringToHash: function stringToHash(s) { var i, stringData = s, result = 0; if (s === null || s === undefined) { return 0; } if (typeof (s) !== "string") { stringData = s.toString(); } if (!stringData) { return 0; } if (typeof (stringData) !== "string") { throw new Error("stringToHash: A string argument expected or any object that implements toString()"); } i = stringData.length - 1; while (i > 0) { result = stringData.charCodeAt(i) * 31 + result; result = result & result; i -= 1; } return result & result; }, /// Compares the first item to the second items and returns true if the two items are equal and false otherwise defaultEqual: function (first, second) { return first === second; }, /// Returns the hash of a given key based on the provided options getHash: function (key, options) { if (options && options.hasOwnProperty("getHashCode")) { return options.getHashCode(key); } if (key.getHashCode !== undefined && typeof (key.getHashCode) === "function") { return key.getHashCode(); } return key.toString(); }, /// Returns the most appropriate equal function based on the options and the key getEqual: function (key, options) { if (options && options.equal) { return options.equal; } else { if (key.equal !== undefined && typeof (key.equal) === "function") { return function(other, unused) { return key.equal(other); }; } else { return statics.defaultEqual; } } }, /// Shallow copies only the relevant properties and functions from the options object copyOptions: function (options) { if (options === undefined || options === null) { return undefined; } var result = {}; if (options.hasOwnProperty("getHashCode")) { result.getHashCode = options.getHashCode; } if (options.hasOwnProperty("equal")) { result.equal = options.equal; } return result; } }; })(); var HashTable = (function () { function HashTable(options) { this._buckets = { }; this._count = 0; this._options = statics.copyOptions(options); } //Static functions //Searches the given key within the given bucket. If the key is found then returns the index in the bucket, //otherwise returns -1 HashTable.getKeyIndex = function (bucket, key, options) { var i, bucketLength = bucket.length, equality, currentItem; equality = statics.getEqual(key, options); for (i = 0; i < bucketLength; i += 1) { currentItem = bucket[i]; if (currentItem !== undefined && equality(currentItem._key, key)) { return i; } } return -1; }; ///A static function that adds the given key value pair to the given bucket HashTable.addToBucket = function (bucket, key, value, options) { bucket.push(new KeyValuePair(key, value)); }; //Removes the key from the given bucket. Returns false if the key was not found in the bucket HashTable.removeKey = function (bucket, key, options) { var index = HashTable.getKeyIndex(bucket, key, options), bucketLength = bucket.length; if (index < 0) { return false; } if (bucketLength > 1 && index !== (bucketLength - 1)) { bucket[index] = bucket[bucketLength - 1]; } bucket.length = bucketLength - 1; return true; }; ///Creates a new HashTable which is a union of the first and second HashTables. You may specify an optional options parameter and ///an optional overwriteIfExists parameter. The options are used to create the result HashTable and all the key-value pairs are added accordingly. //When overwriteIfExists is true and a key from the second HashTable already exists in the first HashTable the entire key-value pair will be overwritten in the result. ///If overwriteIfExists is false then the key-value pair is ignored. HashTable.union = function (first, second, options, overwriteIfExists) { var result = new HashTable(options), keyValuePairs; keyValuePairs = first.getKeyValuePairs(); result.addRange(keyValuePairs, overwriteIfExists); keyValuePairs = second.getKeyValuePairs(); result.addRange(keyValuePairs, overwriteIfExists); return result; }; ///Creates a new HashTable which is an intersection of the first and second HashTables. You may specify an optional options parameter and ///an optional overwriteIfExists parameter. The options are used to create the result HashTable and all the key-value pairs are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashTable that might have different options (see the add function for details). ///In any case the key-value pairs from the first HashTable are in the result. HashTable.intersection = function (first, second, options, overwriteIfExists) { var firstLength = first.count(), secondLength = second.count(), result, i, keyValuePairs, tempPair; result = new HashTable(options); if (firstLength < secondLength) { keyValuePairs = first.getKeyValuePairs(); for (i = 0; i < firstLength; i += 1) { if (second.contains(keyValuePairs[i].key)) { result.add(keyValuePairs[i].key, keyValuePairs[i].value, overwriteIfExists); } } } else { keyValuePairs = second.getKeyValuePairs(); for (i = 0; i < secondLength; i += 1) { tempPair = first.get(keyValuePairs[i].key); if (tempPair !== null) { result.add(tempPair.key, tempPair.value, overwriteIfExists); } } } return result; }; ///Creates a new HashTable which is the difference of the first and second HashTables (i.e. all the key-value pairs which are in the first HashTable but not in the second HashTable). ///You may specify an optional options parameter and an optional overwriteIfExists parameter. The options are used to create the result HashTable and all the key-value pairs are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashTable that might have different options (see the add function for details). ///In any case the key-value pairs from the first HashTable are in the result. HashTable.difference = function (first, second, options, overwriteIfExists) { var i, length, keyValuePairs, result = new HashTable(options), pair; keyValuePairs = first.getKeyValuePairs(); length = first.count(); for (i = 0; i < length; i += 1) { pair = keyValuePairs[i]; if (!second.contains(pair.key, pair.value)) { result.add(pair.key, pair.value, overwriteIfExists); } } return result; }; ///Creates a new HashTable which is the symmetric difference of the first and second HashTables (i.e. all the key-value pairs which are in the first HashTable but not in the second HashTable //or in the second HashTable but not in the first). ///You may specify an optional options parameter and an optional overwriteIfExists parameter. The options are used to create the result HashTable and all the key-value pairs are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashTable that might have different options (see the add function for details). HashTable.symmetricDifference = function (first, second, options, overwriteIfExists) { var i, length, keyValuePairs, result = new HashTable(options), pair; keyValuePairs = first.getKeyValuePairs(); length = first.count(); for (i = 0; i < length; i += 1) { pair = keyValuePairs[i]; if (!second.contains(pair.key, pair.value)) { result.add(pair.key, pair.value, overwriteIfExists); } } keyValuePairs = second.getKeyValuePairs(); length = second.count(); for (i = 0; i < length; i += 1) { pair = keyValuePairs[i]; if (!first.contains(pair.key, pair.value)) { result.add(pair.key, pair.value, false); } } return result; }; //Prototype functions ///Adds a key value pair to the HashTable, returns true if any item was added and false otherwise. ///you may specify the overwriteIfExists flag. When overwriteIfExists is true the value of the given key will be replaced (the key will also be replaced) ///if this key already exists in the HashTable. When overwriteIfExists is false and the key already exists then nothing will happen but the ///function will return false (since nothing was added) HashTable.prototype.add = function (key, value, overwriteIfExists) { var hash, addedItem, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { this._buckets[hash] = []; } bucket = this._buckets[hash]; itemIndex = HashTable.getKeyIndex(bucket, key, this._options); if (itemIndex >= 0) { if (overwriteIfExists) { bucket[itemIndex] = new KeyValuePair(key, value); return true; } return false; } else { addedItem = HashTable.addToBucket(this._buckets[hash], key, value, this._options); this._count += 1; return true; } }; ///Adds a collection of keys and values to the HashTable, returns the number of items added. ///There are two possible uses: ///1) addRange(keyValuePairs,[overwriteIfExists]) - provide a collection of objects that have key and value property and an optional overwriteIfExists argument (see add for details) ///2) addRange(keys, values, [overwriteIfExists]) - provide two collections (one of keys and the other of values) and an optional overwriteIfExists argument (see add for details) HashTable.prototype.addRange = function (arg1, arg2, arg3) { var i, keysLength, valuesLength, minLength, result = 0; if (arguments.length === 1 || (arguments.length === 2 && (typeof (arguments[1]) === "boolean" || arguments[1] === undefined))) { for (i = 0, keysLength = arg1.length; i < keysLength; i += 1) { if (this.add(arg1[i].key, arg1[i].value, arg2)) { result += 1; } } } else { keysLength = arg1.length; valuesLength = arg2.length; minLength = Math.min(keysLength, valuesLength); for (i = 0; i < minLength; i += 1) { if (this.add(arg1[i], arg2[i], arg3)) { result += 1; } } } return result; }; ///Retrieves the value associated with the given key. If the key doesn't exist null is returned. HashTable.prototype.get = function (key) { var hash, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return null; } bucket = this._buckets[hash]; itemIndex = HashTable.getKeyIndex(bucket, key, this._options); if (itemIndex < 0) { return null; } return { value: bucket[itemIndex]._value, key: bucket[itemIndex]._key }; }; /// Removes the key-value pair with the given key. Returns false if the key wasn't found in the HashTable HashTable.prototype.remove = function (key) { var hash, bucket, keyRemoved; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return false; } bucket = this._buckets[hash]; keyRemoved = HashTable.removeKey(bucket, key, this._options); if (keyRemoved) { this._count -= 1; if (bucket.length === 0) { delete (this._buckets[hash]); } } return keyRemoved; }; /// Returns true if the HashTable contains the key and false otherwise HashTable.prototype.contains = function (key) { var hash, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return false; } bucket = this._buckets[hash]; itemIndex = HashTable.getKeyIndex(bucket, key, this._options); if (itemIndex < 0) { return false; } return true; }; ///Returns all the hashes that are currently in the HashTable HashTable.prototype.getHashes = function () { var result = [], hash; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { result.push(hash); } } return result; }; ///Returns an array of all the key-value pairs in the HashTable HashTable.prototype.getKeyValuePairs = function () { var result = [], hash, bucket, i, bucketLength; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { bucket = this._buckets[hash]; for (i = 0, bucketLength = bucket.length; i < bucketLength; i += 1) { result.push({ key: bucket[i]._key, value: bucket[i]._value }); } } } return result; }; ///Returns the total number of items in the HashTable HashTable.prototype.count = function () { return this._count; }; ///Removes all the items from the HashTable HashTable.prototype.clear = function () { this._count = 0; this._buckets = {}; }; ///Returns a new HashTable which is a shallow copy of this HashTable HashTable.prototype.clone = function () { var result = new HashTable(statics.copyOptions(this._options)), hash, bucket, newBucket, i, bucketLength; result._count = this._count; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { bucket = this._buckets[hash]; bucketLength = bucket.length; newBucket = []; newBucket.length = bucketLength; result._buckets[hash] = newBucket; for (i = 0; i < bucketLength; i += 1) { newBucket[i] = new KeyValuePair(bucket[i]._key, bucket[i]._value); } } } return result; }; ///Returns a new HashTable where all the key value pairs are rehashed according to the new options HashTable.prototype.rehash = function (options, overwriteIfExists) { var result = new HashTable(options), pairs = this.getKeyValuePairs(), i, length = pairs.length, pair; for (i = 0; i < length; i += 1) { pair = pairs[i]; result.add(pair.key, pair.value, overwriteIfExists); } return result; }; ///Prints the content of the HashTable to the console. This is used for debugging HashTable.prototype.print = function () { var hash, bucket, i, length; console.log("Count: ", this._count); for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { console.log("*"); console.log("Bucket:", hash); bucket = this._buckets[hash]; length = bucket.length; console.log("There are", length, "item slots"); for (i = 0; i < length; i += 1) { if (bucket[i] === undefined) { console.log(" ", i, ":", undefined); } else { console.log(" ", i, ":", "Key:", bucket[i]._key, "Value:", bucket[i]._value); } } } } }; return HashTable; })(); var HashSet = (function () { function HashSet(options) { this._buckets = { }; this._count = 0; this._options = statics.copyOptions(options); } //Static functions //Searches the given key within the given bucket. If the key is found then returns the index in the bucket, //otherwise returns -1 HashSet.getKeyIndex = function (bucket, key, options) { var i, bucketLength = bucket.length, equality, currentItem; equality = statics.getEqual(key, options); for (i = 0; i < bucketLength; i += 1) { currentItem = bucket[i]; if (currentItem !== undefined && equality(currentItem, key)) { return i; } } return -1; }; ///A static function that adds the given key to the given bucket HashSet.addToBucket = function (bucket, key, options) { bucket.push(key); }; //Removes the key from the given bucket. Returns false if the key was not found in the bucket HashSet.removeKey = function (bucket, key, options) { var index = HashSet.getKeyIndex(bucket, key, options), bucketLength = bucket.length; if (index < 0) { return false; } if (bucketLength > 1 && index !== (bucketLength - 1)) { bucket[index] = bucket[bucketLength - 1]; } bucket.length = bucketLength - 1; return true; }; ///Creates a new HashSet which is a union of the first and second HashSet. You may specify an optional options parameter and ///an optional overwriteIfExists parameter. The options are used to create the result HashSet and all the keys added accordingly. //When overwriteIfExists is true and a key from the second HashSet already exists in the first HashTable it will be overwritten in the result. ///If overwriteIfExists is false then the key is ignored. HashSet.union = function (first, second, options, overwriteIfExists) { var result = new HashSet(options), keys; keys = first.getKeys(); result.addRange(keys, overwriteIfExists); keys = second.getKeys(); result.addRange(keys); return result; }; ///Creates a new HashSet which is an intersection of the first and second HashSets. You may specify an optional options parameter and ///an optional overwriteIfExists parameter. The options are used to create the result HashSet and all the keys are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashSet that might have different options (see the add function for details). ///In any case the keys from the first HashSet are in the result. HashSet.intersection = function (first, second, options, overwriteIfExists) { var firstLength = first.count(), secondLength = second.count(), result, i, keys, tempKey; result = new HashSet(options); if (firstLength < secondLength) { keys = first.getKeys(); for (i = 0; i < firstLength; i += 1) { tempKey = keys[i]; if (second.contains(tempKey)) { result.add(tempKey, overwriteIfExists); } } } else { keys = second.getKeys(); for (i = 0; i < secondLength; i += 1) { tempKey = first.get(keys[i]); if (tempKey !== null) { result.add(tempKey, overwriteIfExists); } } } return result; }; ///Creates a new HashSet which is the difference of the first and second HashSets (i.e. all the keys which are in the first HashSet but not in the second HashSet). ///You may specify an optional options parameter and an optional overwriteIfExists parameter. The options are used to create the result HashSet and all the keys are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashSet that might have different options (see the add function for details). ///In any case the keys from the first HashSet are in the result. HashSet.difference = function (first, second, options, overwriteIfExists) { var i, length, keys, result = new HashSet(options), tempKey; keys = first.getKeys(); length = first.count(); for (i = 0; i < length; i += 1) { tempKey = keys[i]; if (!second.contains(tempKey)) { result.add(tempKey, overwriteIfExists); } } return result; }; ///Creates a new HashSet which is the symmetric difference of the first and second HashSets (i.e. all the keys which are in the first HashSet but not in the second HashSet //or in the second HashSet but not in the first). ///You may specify an optional options parameter and an optional overwriteIfExists parameter. The options are used to create the result HashSet and all the keys are added accordingly. ///overwriteIfExists is used to add the elements to the resulting HashSet that might have different options (see the add function for details). HashSet.symmetricDifference = function (first, second, options, overwriteIfExists) { var i, length, keys, result = new HashSet(options), tempKey; keys = first.getKeys(); length = first.count(); for (i = 0; i < length; i += 1) { tempKey = keys[i]; if (!second.contains(tempKey)) { result.add(tempKey, overwriteIfExists); } } keys = second.getKeys(); length = second.count(); for (i = 0; i < length; i += 1) { tempKey = keys[i]; if (!first.contains(tempKey)) { result.add(tempKey, false); } } return result; }; //Prototype functions ///Adds a key to the HashSet, returns true if any item was added and false otherwise. ///you may specify the overwriteIfExists flag. When overwriteIfExists is true the key will be replaced ///if this key already exists in the HashSet. When overwriteIfExists is false and the key already exists then nothing ///will happen but the function will return false (since nothing was added) HashSet.prototype.add = function (key, overwriteIfExists) { var hash, addedItem, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { this._buckets[hash] = []; } bucket = this._buckets[hash]; itemIndex = HashSet.getKeyIndex(bucket, key, this._options); if (itemIndex >= 0) { if (overwriteIfExists) { bucket[itemIndex] = key; return true; } return false; } else { addedItem = HashSet.addToBucket(this._buckets[hash], key, this._options); this._count += 1; return true; } }; ///Adds a collection of keys to the HashSet, returns the number of keys added. ///you may specify the overwriteIfExists flag. When overwriteIfExists is true the key will be replaced ///if this key already exists in the HashSet. When overwriteIfExists is false and the key already exists then nothing ///will happen. HashSet.prototype.addRange = function (keys, overwriteIfExists) { var i, length, result = 0; for (i = 0, length = keys.length; i < length; i += 1) { if (this.add(keys[i], overwriteIfExists)) { result += 1; } } return result; }; ///Retrieves the key which is equal to the given key. If the key doesn't exist null is returned. HashSet.prototype.get = function (key) { var hash, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return null; } bucket = this._buckets[hash]; itemIndex = HashSet.getKeyIndex(bucket, key, this._options); if (itemIndex < 0) { return null; } return bucket[itemIndex]; }; /// Removes the key. Returns false if the key wasn't found in the HashSet HashSet.prototype.remove = function (key) { var hash, bucket, keyRemoved; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return false; } bucket = this._buckets[hash]; keyRemoved = HashSet.removeKey(bucket, key, this._options); if (keyRemoved) { this._count -= 1; if (bucket.length === 0) { delete (this._buckets[hash]); } } return keyRemoved; }; /// Returns true if the HashSet contains the key and false otherwise HashSet.prototype.contains = function (key) { var hash, bucket, itemIndex; if (!statics.verifyKey) { return false; } hash = statics.getHash(key, this._options).toString(); if (!this._buckets.hasOwnProperty(hash)) { return false; } bucket = this._buckets[hash]; itemIndex = HashSet.getKeyIndex(bucket, key, this._options); if (itemIndex < 0) { return false; } return true; }; ///Returns all the hashes that are currently in the HashSet HashSet.prototype.getHashes = function () { var result = [], hash; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { result.push(hash); } } return result; }; ///Returns an array of all the keys in the HashSet HashSet.prototype.getKeys = function () { var result = [], hash, bucket, i, bucketLength; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { bucket = this._buckets[hash]; for (i = 0, bucketLength = bucket.length; i < bucketLength; i += 1) { result.push(bucket[i]); } } } return result; }; ///Returns the total number of items in the HashSet HashSet.prototype.count = function () { return this._count; }; ///Removes all the items from the HashSet HashSet.prototype.clear = function () { this._count = 0; this._buckets = {}; }; ///Returns a new HashSet which is a shallow copy of this HashSet HashSet.prototype.clone = function () { var result = new HashSet(statics.copyOptions(this._options)), hash, bucket, newBucket, i, bucketLength; result._count = this._count; for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { bucket = this._buckets[hash]; bucketLength = bucket.length; newBucket = []; newBucket.length = bucketLength; result._buckets[hash] = newBucket; for (i = 0; i < bucketLength; i += 1) { newBucket[i] = bucket[i]; } } } return result; }; ///Returns a new HashSet where all the keys are rehashed according to the new options HashSet.prototype.rehash = function (options, overwriteIfExists) { var result = new HashSet(options), keys = this.getKeys(), i, length = keys.length; for (i = 0; i < length; i += 1) { result.add(keys[i], overwriteIfExists); } return result; }; ///Prints the content of the HashSet to the console. This is used for debugging HashSet.prototype.print = function () { var hash, bucket, i, length; console.log("Count: ", this._count); for (hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { console.log("*"); console.log("Bucket:", hash); bucket = this._buckets[hash]; length = bucket.length; console.log("There are", length, "item slots"); for (i = 0; i < length; i += 1) { if (bucket[i] === undefined) { console.log(" ", i, ":", undefined); } else { console.log(" ", i, ":", "Key:", bucket[i]); } } } } }; return HashSet; })(); exports.statics = statics; exports.HashTable = HashTable; exports.HashSet = HashSet;