API Docs for: 1.1.0
Show:

File: src\MultiSet.js

/*
 *	Canvace's utility class MultiSet
 *	Copyright (C) 2013  Canvace Srl  <http://www.canvace.com/>
 *
 *	Dual licensed under the MIT and GPLv3 licenses.
 *
 *	This program is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	This program is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Implements a multi-set, a set with possibly repeated elements.
 *
 * This container allows insertion and removal of elements in constant time
 * assuming the implementation of the underlying JavaScript engine manages in
 * amortizing insertion and removal of properties in objects to constant time.
 *
 * The arguments you specify to the MultiSet constructor are immediately
 * inserted into the container. For example:
 *
 *	var ms = new MultiSet(1, 2, 3); // ms contains 1, 2 and 3
 *
 * The previous snippet is equivalent to:
 *
 *	var ms = new MultiSet();
 *	ms.add(1);
 *	ms.add(2);
 *	ms.add(3);
 *
 * @class MultiSet
 * @constructor
 * @param [elements]* {Object} The elements to add initially to the `MultiSet`.
 */
function MultiSet() {
	'use strict';
	this.elements = {};
	this.nextId = 0;
	this.count = 0;
	this.resetCount = 0;
	this.fastAdd.apply(this, arguments);
}

/**
 * Inserts an element into the container in amortized constant time. The
 * element can be of any type (numbers, strings, objects, etc.) and can be
 * inserted many times.
 *
 * The {{#crossLink "MultiSet/add"}}{{/crossLink}} method returns a function
 * that removes the element. The returned function is idempotent: it does not
 * have any effect when called again after the first time.
 *
 * Example:
 *
 *	var ms = new MultiSet(1, 2);
 *	var remove = ms.add(3); // ms now contains three elements: 1, 2 and 3
 *	remove(); // ms now contains two elements: 1 and 2
 *	remove(); // no effect, ms still contains 1 and 2
 *
 * The returned function returns a boolean value indicating whether the element
 * was present and could be removed or not. `false` indicates the element was
 * not present because it had already been removed by a previous call.
 *
 * Example:
 *
 *	var ms = new MultiSet();
 *	var remove = ms.add(3);
 *	if (remove()) {
 *		alert('removed!');
 *	}
 *	if (remove()) {
 *		alert('this is never alerted');
 *	}
 *
 * @method add
 * @param element {Any} The element to be inserted in the
 * {{#crossLink "MultiSet"}}MultiSet{{/crossLink}}.
 * @return {Function} A function that removes the inserted element.
 */
MultiSet.prototype.add = function (element) {
	'use strict';
	var id = this.nextId++;
	this.elements[id] = element;
	this.count++;
	var resetCount = this.resetCount;
	return function () {
		if (resetCount < this.resetCount) {
			return false;
		} else {
			if (this.elements.hasOwnProperty(id)) {
				delete this.elements[id];
				this.count--;
				return true;
			} else {
				return false;
			}
		}
	}.bind(this);
};

/**
 * Inserts zero or more elements into the container in amortized constant time.
 * The elements can be of any type (numbers, strings, objects, etc.) and can be
 * inserted many times.
 *
 * This method is faster than {{#crossLink "MultiSet/add"}}add{{/crossLink}}
 * because it doesn't generate any closures; infact it doesn't return anything.
 *
 * @method fastAdd
 * @param [elements...] {Any} Zero or more elements to insert in the
 * {{#crossLink "MultiSet"}}MultiSet{{/crossLink}}.
 * @example
 *	var ms = new MultiSet(1, 2);
 *	ms.fastAdd(3, 4); // ms now contains four elements: 1, 2, 3 and 4
 */
MultiSet.prototype.fastAdd = function () {
	'use strict';
	for (var i = 0; i < arguments.length; i++) {
		this.elements[this.nextId++] = arguments[i];
	}
	this.count += arguments.length;
};

/**
 * Iterates over the container and calls the specified function `action` for
 * each iterated element.
 *
 * The `action` function receives two arguments: the element and a function that
 * removes it if called. The removing function stays valid forever, even after
 * the whole `forEach` call is over, and is idempotent: it does not have any
 * effects after it is called once.
 *
 * The following example inserts some numbers into the container and then
 * removes only the numbers equal to 3:
 *
 *	var ms = new MultiSet(1, 3, 7, 6, 3, 4, 3, 3, 5);
 *	ms.forEach(function (element, remove) {
 *		if (element === 3) {
 *			remove();
 *		}
 *	});
 *	// ms now contains 1, 7, 6, 4, 5
 *
 * Elements with repetitions are iterated as many times as they are repeated.
 * For example, in the previous snippet the number 3 is iterated (and removed)
 * four times.
 *
 * Note that the order of iteration is undefined as it depends on the order of
 * iteration over object properties implemented by the underlying JavaScript
 * engine. This is typically the insertion order, which means
 * {{#crossLink "MultiSet/forEach"}}forEach{{/crossLink}} enumerates the
 * elements in the same order they are inserted by
 * {{#crossLink "MultiSet/add"}}add{{/crossLink}}, but you must not depend on
 * that assumption.
 *
 * The iteration is interrupted if the `action` function returns `false`. The
 * following example adds some numbers to the container, then iterates over it
 * and interrupts when it encounters the number 3:
 *
 *	var ms = new MultiSet(1, 2, 3, 4);
 *	ms.forEach(function (element) {
 *		if (element === 3) {
 *			return false;
 *		}
 *	});
 *
 * The number 4 is not enumerated.
 *
 * {{#crossLink "MultiSet/forEach"}}forEach{{/crossLink}} returns `false` if the
 * iteration completed and `true` if it was interrupted, which is suitable for
 * implementing finding algorithms.
 *
 * @method forEach
 * @param action {Function} The callback function that gets called for each
 * element of the multiset. It receives the current element and a callback
 * function suitable for deleting it from the
 * {{#crossLink "MultiSet"}}MultiSet{{/crossLink}}.
 * @param action.element {Any} The current element of the iteration.
 * @param action.remove {Function} A function that removes the current element.
 * @return {Boolean} `true` if `action` returned `false`, `false` if it did not
 * and all the elements were enumerated.
 */
MultiSet.prototype.forEach = function (action) {
	'use strict';
	for (var id in this.elements) {
		if (this.elements.hasOwnProperty(id)) {
			if (action(this.elements[id], function (id) {
				var resetCount = this.resetCount;
				return function () {
					if (resetCount < this.resetCount) {
						return false;
					} else {
						if (this.elements.hasOwnProperty(id)) {
							delete this.elements[id];
							this.count--;
							return true;
						} else {
							return false;
						}
					}
				}.bind(this);
			}.call(this, id)) === false) {
				return true;
			}
		}
	}
	return false;
};

/**
 * Iterates over the container and calls the specified function `action` for
 * each iterated element.
 *
 * The `action` function receives one argument, the current element. Any return
 * value is ignored.
 *
 * This method is similar to the
 * {{#crossLink "MultiSet/forEach"}}forEach{{/crossLink}} method except it can
 * be faster on some browsers because it does not generate a closure (the
 * element's removal function) at each iterated element and does not analyze the
 * return value of the callback function. Infact, the iterated elements cannot
 * be removed and the iteration cannot be interrupted.
 *
 * You usually use the
 * {{#crossLink "MultiSet/forEach"}}forEach{{/crossLink}} method, but you
 * may also use {{#crossLink "MultiSet/fastForEach"}}fastForEach{{/crossLink}}
 * if your callback function does not use its second argument (the removal
 * function) and never returns `false`.
 *
 * Note that the order of iteration is undefined as it depends on the order of
 * iteration over object properties implemented by the underlying JavaScript
 * engine. This is typically the insertion order, which means
 * {{#crossLink "MultiSet/fastForEach"}}fastForEach{{/crossLink}} enumerates the
 * elements in the same order they are inserted by
 * {{#crossLink "MultiSet/add"}}add{{/crossLink}}, but you must not rely on that
 * assumption.
 *
 * @method fastForEach
 * @param action {Function} The callback function that gets called for each
 * @param action.element {Any} The current element of the iteration.
 * element of the multiset. It receives the current element as an argument. The
 * return value is ignored.
 */
MultiSet.prototype.fastForEach = function (action) {
	'use strict';
	for (var id in this.elements) {
		if (this.elements.hasOwnProperty(id)) {
			action(this.elements[id]);
		}
	}
};

/**
 * Returns the number of elements currently contained.
 *
 * If an element is inserted more than once, it counts as many times as it is
 * inserted.
 *
 * This method operates in constant time.
 *
 * @method count
 * @return {Number} The number of contained elements.
 * @example
 *	var ms = new MultiSet(1, 2, 2, 3, 3);
 *	alert(ms.count()); // alerts 5
 */
MultiSet.prototype.count = function () {
	'use strict';
	return this.count;
};

/**
 * Indicates whether the container is empty or not.
 *
 * @method isEmpty
 * @return {Boolean} `true` if the container is empty, `false` otherwise.
 */
MultiSet.prototype.isEmpty = function () {
	'use strict';
	return !this.count;
};

/**
 * Empties the container: every element is removed and the count is reset to
 * zero.
 *
 * This method operates in constant time.
 *
 * @method clear
 * @example
 *	var ms = new MultiSet(1, 2, 3, 4, 5);
 *	ms.clear();
 *	alert(ms.count()); // alerts 0
 */
MultiSet.prototype.clear = function () {
	'use strict';
	this.elements = {};
	this.count = 0;
	this.resetCount++;
};