BitArray.js

import { assertTrue, isInteger } from './util';

/**
 * A {@link BitSet} implementation with no limit due to bits being stored in an array. Also known as bit set, bit map
 * or bit vector. This implementation will never throw out of bounds errors.
 *
 * @public
 * @class
 * @implements {BitSet}
 */
class BitArray {
	/**
	 * An array containing each bit as a boolean value, starting from the rightmost bit.
	 *
	 * @private
	 * @member {Array<Boolean>}
	 */
	value;

	/**
	 * @public
	 * @constructor
	 * @param {Number} [minLength = 1] The minimum length of the bitArray.
	 * @throws {Error} In case 'minLength' is equals to or smaller than zero.
	 */
	constructor(minLength) {
		assertTrue(minLength === undefined || minLength > 0,
			'Illegal argument: parameter \'minLength\' must be larger than 0');

		minLength = minLength || 1;

		this.value = [];

		for (let i = 0; i < minLength; i++) {
			this.value.push(false);
		}
	}

	/**
	 * Combines masks. This is equivalent to a OR operation.
	 *
	 * @private
	 * @static
	 * @param {...BitMask} masks The masks to combine.
	 * @returns {BitArray} The resulting mask.
	 */
	static combineMasks(...masks) {
		const bitArray = new BitArray();
		const tempArray = new BitArray();

		for (const mask of masks) {
			tempArray.copy(mask);

			for (let i = 0; i < tempArray.length; i++) {
				const oldBit = bitArray.get(i);
				const newBit = tempArray.get(i);
				bitArray.setAt(oldBit || newBit, i);
			}
		}

		return bitArray;
	}

	/**
	 * Produces a new BitArray instance from a value. The value may contain anything, the resulting bitArray is based
	 * on the truthiness of the value contents.
	 * Example: [true, 0, {}] will yield 101.
	 *
	 * @public
	 * @static
	 * @param {Array<*>} array
	 * @returns {BitArray} A new BitArray instance.
	 */
	static fromArray(array) {
		const bitArray = new BitArray();

		bitArray.value = array.slice(0).reverse().map(Boolean);

		return bitArray;
	}

	get length() {
		return this.value.length;
	}

	count() {
		return this.value.filter(value => value).length;
	}

	intersect(...masks) {
		const bitArray = BitArray.combineMasks(...masks);
		const length = Math.max(bitArray.length, this.length);

		for (let i = 0; i < length; i++) {
			const thisBit = this.get(i);
			const otherBit = bitArray.get(i);

			this.setAt(thisBit && otherBit, i);
		}

		return this;
	}

	intersects(...masks) {
		const bitArray = BitArray.combineMasks(...masks);
		const length = Math.max(bitArray.length, this.length);

		for (let i = 0; i < length; i++) {
			const thisBit = this.get(i);
			const otherBit = bitArray.get(i);

			if (thisBit && otherBit) {
				return true;
			}
		}

		return false;
	}

	get(index) {
		assertTrue(isInteger(index), 'Illegal argument: parameter \'index\' is not an integer');

		return this.value[index] || false;
	}

	getRange(from, to) {
		assertTrue(isInteger(from), 'Illegal argument: parameter \'from\' is not an integer');
		assertTrue(isInteger(to), 'Illegal argument: parameter \'to\' is not an integer');
		assertTrue(to > from, 'Illegal argument: parameter \'to\' must be larger than parameter \'from\'');

		const length = to - from;
		const bitArray = new BitArray();
		bitArray.value = this.value.slice(from, to);

		while (bitArray.length < length) {
			bitArray.value.push(false);
		}

		return bitArray;
	}

	test(...masks) {
		const mask = BitArray.combineMasks(...masks);

		for (let i = 0; i < mask.length; i++) {
			const bit = mask.get(i);

			if (bit && !this.get(i)) {
				return false;
			}
		}

		return true;
	}

	testAny(...masks) {
		const mask = BitArray.combineMasks(...masks);

		for (let i = 0; i < mask.length; i++) {
			const bit = mask.get(i);

			if (bit && this.get(i)) {
				return true;
			}
		}

		return false;
	}

	testAt(value, index) {
		assertTrue(isInteger(index), 'Illegal argument: parameter \'index\' is not an integer');

		if (index > this.length) {
			return Boolean(value) === false;
		}

		return Boolean(value) === this.get(index);
	}

	testAll(value) {
		value = Boolean(value);

		return this.value.every(element => element === value);
	}

	on(...masks) {
		return this.set(1, ...masks);
	}

	off(...masks) {
		return this.set(0, ...masks);
	}

	set(value, ...masks) {
		const mask = BitArray.combineMasks(...masks);

		for (let i = 0; i < mask.length; i++) {
			const bit = mask.get(i);

			if (bit) {
				this.setAt(value, i);
			}
		}

		return this;
	}

	setAll(value) {
		this.value.fill(Boolean(value));

		return this;
	}

	setAt(value, index) {
		assertTrue(isInteger(index), 'Illegal argument: parameter \'index\' is not an integer');

		while (index >= this.length) {
			this.value.push(false);
		}

		this.value[index] = Boolean(value);

		return this;
	}

	setRange(value, from, to) {
		assertTrue(isInteger(from), 'Illegal argument: parameter \'from\' is not an integer');
		assertTrue(isInteger(to), 'Illegal argument: parameter \'to\' is not an integer');
		assertTrue(to > from, 'Illegal argument: parameter \'to\' must be larger than parameter \'from\'');

		for (let i = from; i < to; i++) {
			this.setAt(value, i);
		}

		return this;
	}

	flip(...masks) {
		const mask = BitArray.combineMasks(...masks);

		for (let i = 0; i < mask.length; i++) {
			const bit = mask.get(i);

			if (bit) {
				this.flipAt(i);
			}
		}

		return this;
	}

	flipAll() {
		for (let i = 0; i < this.length; i++) {
			this.flipAt(i);
		}

		return this;
	}

	flipAt(index) {
		assertTrue(isInteger(index), 'Illegal argument: parameter \'index\' is not an integer');

		while (index >= this.length) {
			this.value.push(false);
		}

		this.setAt(!this.get(index), index);

		return this;
	}

	flipRange(from, to) {
		assertTrue(isInteger(from), 'Illegal argument: parameter \'from\' is not an integer');
		assertTrue(isInteger(to), 'Illegal argument: parameter \'to\' is not an integer');
		assertTrue(to > from, 'Illegal argument: parameter \'to\' must be larger than parameter \'from\'');

		for (let i = from; i < to; i++) {
			this.flipAt(i);
		}

		return this;
	}

	copy(bitset) {
		this.value.fill(false);

		if (bitset instanceof Object) {
			const array = bitset.toArray().reverse();
			this.value.splice(0, array.count, ...array);
		} else {
			let index = 0;

			while (bitset > 0) {
				this.setAt(bitset & 1, index);
				bitset >>= 1;
				index++;
			}
		}

		return this;
	}

	/**
	 * Gets the integer value of this instance.
	 *
	 * @public
	 * @throws {Error} In case this instance cannot be represented by an integer (by exceeding 31 bits).
	 * @returns {Number}
	 */
	valueOf() {
		if (this.length > 31) {
			throw new Error('Number exceeds 31 bits');
		}

		return this.toArray().reduce((prev, curr) => {
			prev <<= 1;

			if (curr) {
				prev++;
			}

			return prev;
		}, 0);
	}

	serialize() {
		return this.toArray().map(Number).join('');
	}

	/**
	 * Deserializes a string and returns a new instance.
	 *
	 * @public
	 * @static
	 * @param {String} input
	 * @throws {Error} In case input could not be parsed.
	 * @returns {BitArray} A new instance.
	 */
	static deserialize(input) {
		const array = input.split('');

		if (array.some(isNaN)) {
			throw new Error('Failed to deserialize input');
		}

		return BitArray.fromArray(array.map(Number));
	}

	clone() {
		return new BitArray(this.length).copy(this);
	}

	equals(other) {
		const bitArray = new BitArray().copy(other);
		const length = Math.max(this.length, bitArray.length);

		for (let i = 0; i < length; i++) {
			if (this.get(i) !== bitArray.get(i)) {
				return false;
			}
		}

		return true;
	}

	toArray() {
		return this.value.slice(0).reverse();
	}

	toString() {
		return `BitArray(${this.serialize()})`;
	}
}

export default BitArray;