You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					446 lines
				
				8.9 KiB
			
		
		
			
		
	
	
					446 lines
				
				8.9 KiB
			| 
											3 years ago
										 | /** | ||
|  |  * Mnemonist MultiSet | ||
|  |  * ==================== | ||
|  |  * | ||
|  |  * JavaScript implementation of a MultiSet. | ||
|  |  */ | ||
|  | var Iterator = require('obliterator/iterator'), | ||
|  |     forEach = require('obliterator/foreach'), | ||
|  |     FixedReverseHeap = require('./fixed-reverse-heap.js'); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Helpers. | ||
|  |  */ | ||
|  | var MULTISET_ITEM_COMPARATOR = function(a, b) { | ||
|  |   if (a[1] > b[1]) | ||
|  |     return -1; | ||
|  |   if (a[1] < b[1]) | ||
|  |     return 1; | ||
|  | 
 | ||
|  |   return 0; | ||
|  | }; | ||
|  | 
 | ||
|  | // TODO: helper functions: union, intersection, sum, difference, subtract
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * MultiSet. | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  */ | ||
|  | function MultiSet() { | ||
|  |   this.items = new Map(); | ||
|  | 
 | ||
|  |   Object.defineProperty(this.items, 'constructor', { | ||
|  |     value: MultiSet, | ||
|  |     enumerable: false | ||
|  |   }); | ||
|  | 
 | ||
|  |   this.clear(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to clear the structure. | ||
|  |  * | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | MultiSet.prototype.clear = function() { | ||
|  | 
 | ||
|  |   // Properties
 | ||
|  |   this.size = 0; | ||
|  |   this.dimension = 0; | ||
|  |   this.items.clear(); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to add an item to the set. | ||
|  |  * | ||
|  |  * @param  {any}    item  - Item to add. | ||
|  |  * @param  {number} count - Optional count. | ||
|  |  * @return {MultiSet} | ||
|  |  */ | ||
|  | MultiSet.prototype.add = function(item, count) { | ||
|  |   if (count === 0) | ||
|  |     return this; | ||
|  | 
 | ||
|  |   if (count < 0) | ||
|  |     return this.remove(item, -count); | ||
|  | 
 | ||
|  |   count = count || 1; | ||
|  | 
 | ||
|  |   if (typeof count !== 'number') | ||
|  |     throw new Error('mnemonist/multi-set.add: given count should be a number.'); | ||
|  | 
 | ||
|  |   this.size += count; | ||
|  | 
 | ||
|  |   const currentCount = this.items.get(item); | ||
|  | 
 | ||
|  |   if (currentCount === undefined) | ||
|  |     this.dimension++; | ||
|  |   else | ||
|  |     count += currentCount; | ||
|  | 
 | ||
|  |   this.items.set(item, count); | ||
|  | 
 | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to set the multiplicity of an item in the set. | ||
|  |  * | ||
|  |  * @param  {any}    item  - Target item. | ||
|  |  * @param  {number} count - Desired multiplicity. | ||
|  |  * @return {MultiSet} | ||
|  |  */ | ||
|  | MultiSet.prototype.set = function(item, count) { | ||
|  |   var currentCount; | ||
|  | 
 | ||
|  |   if (typeof count !== 'number') | ||
|  |     throw new Error('mnemonist/multi-set.set: given count should be a number.'); | ||
|  | 
 | ||
|  |   // Setting an item to 0 or to a negative number means deleting it from the set
 | ||
|  |   if (count <= 0) { | ||
|  |     currentCount = this.items.get(item); | ||
|  | 
 | ||
|  |     if (typeof currentCount !== 'undefined') { | ||
|  |       this.size -= currentCount; | ||
|  |       this.dimension--; | ||
|  |     } | ||
|  | 
 | ||
|  |     this.items.delete(item); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   count = count || 1; | ||
|  | 
 | ||
|  |   currentCount = this.items.get(item); | ||
|  | 
 | ||
|  |   if (typeof currentCount === 'number') { | ||
|  |     this.items.set(item, currentCount + count); | ||
|  |   } | ||
|  |   else { | ||
|  |     this.dimension++; | ||
|  |     this.items.set(item, count); | ||
|  |   } | ||
|  | 
 | ||
|  |   this.size += count; | ||
|  | 
 | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to return whether the item exists in the set. | ||
|  |  * | ||
|  |  * @param  {any} item  - Item to check. | ||
|  |  * @return {boolan} | ||
|  |  */ | ||
|  | MultiSet.prototype.has = function(item) { | ||
|  |   return this.items.has(item); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to delete an item from the set. | ||
|  |  * | ||
|  |  * @param  {any} item  - Item to delete. | ||
|  |  * @return {boolan} | ||
|  |  */ | ||
|  | MultiSet.prototype.delete = function(item) { | ||
|  |   var count = this.items.get(item); | ||
|  | 
 | ||
|  |   if (count === 0) | ||
|  |     return false; | ||
|  | 
 | ||
|  |   this.size -= count; | ||
|  |   this.dimension--; | ||
|  |   this.items.delete(item); | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to remove an item from the set. | ||
|  |  * | ||
|  |  * @param  {any} item  - Item to delete. | ||
|  |  * @param  {number} count - Optional count. | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | MultiSet.prototype.remove = function(item, count) { | ||
|  |   if (count === 0) | ||
|  |     return; | ||
|  | 
 | ||
|  |   if (count < 0) | ||
|  |     return this.add(item, -count); | ||
|  | 
 | ||
|  |   count = count || 1; | ||
|  | 
 | ||
|  |   if (typeof count !== 'number') | ||
|  |     throw new Error('mnemonist/multi-set.remove: given count should be a number.'); | ||
|  | 
 | ||
|  |   var currentCount = this.items.get(item); | ||
|  | 
 | ||
|  |   if (typeof currentCount === 'undefined') return; | ||
|  | 
 | ||
|  |   var newCount = Math.max(0, currentCount - count); | ||
|  | 
 | ||
|  |   if (newCount === 0) { | ||
|  |     this.items.delete(item); | ||
|  |     this.size -= currentCount; | ||
|  |     this.dimension--; | ||
|  |   } | ||
|  |   else { | ||
|  |     this.items.set(item, newCount); | ||
|  |     this.size -= count; | ||
|  |   } | ||
|  | 
 | ||
|  |   return; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to change a key into another one, merging counts if the target | ||
|  |  * key already exists. | ||
|  |  * | ||
|  |  * @param  {any} a - From key. | ||
|  |  * @param  {any} b - To key. | ||
|  |  * @return {MultiSet} | ||
|  |  */ | ||
|  | MultiSet.prototype.edit = function(a, b) { | ||
|  |   var am = this.multiplicity(a); | ||
|  | 
 | ||
|  |   // If a does not exist in the set, we can stop right there
 | ||
|  |   if (am === 0) | ||
|  |     return; | ||
|  | 
 | ||
|  |   var bm = this.multiplicity(b); | ||
|  | 
 | ||
|  |   this.items.set(b, am + bm); | ||
|  |   this.items.delete(a); | ||
|  | 
 | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to return the multiplicity of the given item. | ||
|  |  * | ||
|  |  * @param  {any} item  - Item to get. | ||
|  |  * @return {number} | ||
|  |  */ | ||
|  | MultiSet.prototype.multiplicity = function(item) { | ||
|  |   var count = this.items.get(item); | ||
|  | 
 | ||
|  |   if (typeof count === 'undefined') | ||
|  |     return 0; | ||
|  | 
 | ||
|  |   return count; | ||
|  | }; | ||
|  | MultiSet.prototype.get = MultiSet.prototype.multiplicity; | ||
|  | MultiSet.prototype.count = MultiSet.prototype.multiplicity; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to return the frequency of the given item in the set. | ||
|  |  * | ||
|  |  * @param  {any} item - Item to get. | ||
|  |  * @return {number} | ||
|  |  */ | ||
|  | MultiSet.prototype.frequency = function(item) { | ||
|  |   if (this.size === 0) | ||
|  |     return 0; | ||
|  | 
 | ||
|  |   var count = this.multiplicity(item); | ||
|  | 
 | ||
|  |   return count / this.size; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to return the n most common items from the set. | ||
|  |  * | ||
|  |  * @param  {number} n - Number of items to retrieve. | ||
|  |  * @return {array} | ||
|  |  */ | ||
|  | MultiSet.prototype.top = function(n) { | ||
|  |   if (typeof n !== 'number' || n <= 0) | ||
|  |     throw new Error('mnemonist/multi-set.top: n must be a number > 0.'); | ||
|  | 
 | ||
|  |   var heap = new FixedReverseHeap(Array, MULTISET_ITEM_COMPARATOR, n); | ||
|  | 
 | ||
|  |   var iterator = this.items.entries(), | ||
|  |       step; | ||
|  | 
 | ||
|  |   while ((step = iterator.next(), !step.done)) | ||
|  |     heap.push(step.value); | ||
|  | 
 | ||
|  |   return heap.consume(); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to iterate over the set's values. | ||
|  |  * | ||
|  |  * @param  {function}  callback - Function to call for each item. | ||
|  |  * @param  {object}    scope    - Optional scope. | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | MultiSet.prototype.forEach = function(callback, scope) { | ||
|  |   scope = arguments.length > 1 ? scope : this; | ||
|  | 
 | ||
|  |   var i; | ||
|  | 
 | ||
|  |   this.items.forEach(function(multiplicity, value) { | ||
|  | 
 | ||
|  |     for (i = 0; i < multiplicity; i++) | ||
|  |       callback.call(scope, value, value); | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to iterate over the set's multiplicities. | ||
|  |  * | ||
|  |  * @param  {function}  callback - Function to call for each multiplicity. | ||
|  |  * @param  {object}    scope    - Optional scope. | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | MultiSet.prototype.forEachMultiplicity = function(callback, scope) { | ||
|  |   scope = arguments.length > 1 ? scope : this; | ||
|  | 
 | ||
|  |   this.items.forEach(callback, scope); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the set's keys. I.e. its unique values, | ||
|  |  * in a sense. | ||
|  |  * | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | MultiSet.prototype.keys = function() { | ||
|  |   return this.items.keys(); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the set's values. | ||
|  |  * | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | MultiSet.prototype.values = function() { | ||
|  |   var iterator = this.items.entries(), | ||
|  |       inContainer = false, | ||
|  |       step, | ||
|  |       value, | ||
|  |       multiplicity, | ||
|  |       i; | ||
|  | 
 | ||
|  |   return new Iterator(function next() { | ||
|  |     if (!inContainer) { | ||
|  |       step = iterator.next(); | ||
|  | 
 | ||
|  |       if (step.done) | ||
|  |         return {done: true}; | ||
|  | 
 | ||
|  |       inContainer = true; | ||
|  |       value = step.value[0]; | ||
|  |       multiplicity = step.value[1]; | ||
|  |       i = 0; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (i >= multiplicity) { | ||
|  |       inContainer = false; | ||
|  |       return next(); | ||
|  |     } | ||
|  | 
 | ||
|  |     i++; | ||
|  | 
 | ||
|  |     return { | ||
|  |       done: false, | ||
|  |       value: value | ||
|  |     }; | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the set's multiplicities. | ||
|  |  * | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | MultiSet.prototype.multiplicities = function() { | ||
|  |   return this.items.entries(); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Attaching the #.entries method to Symbol.iterator if possible. | ||
|  |  */ | ||
|  | if (typeof Symbol !== 'undefined') | ||
|  |   MultiSet.prototype[Symbol.iterator] = MultiSet.prototype.values; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Convenience known methods. | ||
|  |  */ | ||
|  | MultiSet.prototype.inspect = function() { | ||
|  |   return this.items; | ||
|  | }; | ||
|  | 
 | ||
|  | if (typeof Symbol !== 'undefined') | ||
|  |   MultiSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = MultiSet.prototype.inspect; | ||
|  | MultiSet.prototype.toJSON = function() { | ||
|  |   return this.items; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Static @.from function taking an arbitrary iterable & converting it into | ||
|  |  * a structure. | ||
|  |  * | ||
|  |  * @param  {Iterable} iterable - Target iterable. | ||
|  |  * @return {MultiSet} | ||
|  |  */ | ||
|  | MultiSet.from = function(iterable) { | ||
|  |   var set = new MultiSet(); | ||
|  | 
 | ||
|  |   forEach(iterable, function(value) { | ||
|  |     set.add(value); | ||
|  |   }); | ||
|  | 
 | ||
|  |   return set; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Function returning whether the multiset A is a subset of the multiset B. | ||
|  |  * | ||
|  |  * @param  {MultiSet} A - First set. | ||
|  |  * @param  {MultiSet} B - Second set. | ||
|  |  * @return {boolean} | ||
|  |  */ | ||
|  | MultiSet.isSubset = function(A, B) { | ||
|  |   var iterator = A.multiplicities(), | ||
|  |       step, | ||
|  |       key, | ||
|  |       mA; | ||
|  | 
 | ||
|  |   // Shortcuts
 | ||
|  |   if (A === B) | ||
|  |     return true; | ||
|  | 
 | ||
|  |   if (A.dimension > B.dimension) | ||
|  |     return false; | ||
|  | 
 | ||
|  |   while ((step = iterator.next(), !step.done)) { | ||
|  |     key = step.value[0]; | ||
|  |     mA = step.value[1]; | ||
|  | 
 | ||
|  |     if (B.multiplicity(key) < mA) | ||
|  |       return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Function returning whether the multiset A is a superset of the multiset B. | ||
|  |  * | ||
|  |  * @param  {MultiSet} A - First set. | ||
|  |  * @param  {MultiSet} B - Second set. | ||
|  |  * @return {boolean} | ||
|  |  */ | ||
|  | MultiSet.isSuperset = function(A, B) { | ||
|  |   return MultiSet.isSubset(B, A); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Exporting. | ||
|  |  */ | ||
|  | module.exports = MultiSet; |