BitField.js

/**
 * A {@link BitSet} implementation limited to 31 bits due to bits being stored in a Number type.
 * This implementation is about 25% faster than a BitArray.
 *
 * @public
 * @class
 * @implements {BitSet}
 */
class BitField {
	/**
	 * The bitfields' current value.
	 *
	 * @private
	 * @member {Number}
	 */
	value = 0;

	/**
	 * The bitfields' minimum length.
	 * Example: a bitfield with a value of 110 (base 2) and a minimum length of 4 makes it so that the value is treated
	 * as 0110. Thus the flipAll method will yield 1001, instead of 001, which would be the result without pre-assigned
	 * length.
	 *
	 * @private
	 * @readonly
	 * @member {Number}
	 */
	minLength;

	/**
	 * @public
	 * @constructor
	 * @param {Number} [minLength = 1] The minimum length of the bitfield.
	 * @throws {Error} In case length exceeds 31 (consider using BitArray instead if u may reach this limit).
	 * @throws {Error} In case 'minLength' is equals to or smaller than zero.
	 */
	constructor(minLength) {
		if (minLength !== undefined && minLength <= 0) {
			throw Error('Illegal argument: parameter \'minLength\' must be larger than 0');
		}

		this.minLength = minLength || 1;

		if (this.minLength > 31) {
			throw new Error('BitField is limited to 31 flags');
		}
	}

	/**
	 * Gets the integer value of a bitsetlike value or instance.
	 *
	 * @private
	 * @static
	 * @param {BitSetLike} value
	 * @returns {Number} The value.
	 */
	static valueOf(value) {
		if (value instanceof Object) {
			return value.valueOf();
		}

		return value;
	}

	/**
	 * Combines masks. This is equivalent to a OR operation.
	 *
	 * @private
	 * @static
	 * @param {...BitMask} masks The masks to combine.
	 * @returns {Number} The resulting mask.
	 */
	static combineMasks(...masks) {
		return masks.reduce((prev, curr) => prev | curr, 0);
	}

	/**
	 * Produces a new BitField instance from an array. The value may contain anything, the resulting bitfield is based
	 * on the truthiness of the value contents.
	 * Example: [true, 0, {}] will yield 101.
	 *
	 * @public
	 * @static
	 * @param {Array<*>} array
	 * @throws {Error} In case length exceeds 31 (consider using BitArray instead if u may reach this limit).
	 * @returns {BitField} A new BitField instance.
	 */
	static fromArray(array) {
		let length = 0;

		const bitMask = array.reduce((prev, curr) => {
			length++;
			prev <<= 1;

			if (curr) {
				prev++;
			}

			return prev;
		}, 0);

		return new BitField(length).on(bitMask);
	}

	get length() {
		let { value } = this;
		let length = 0;

		while (value > 0) {
			length++;
			value >>= 1;
		}

		return Math.max(this.minLength, length);
	}

	count() {
		let { value } = this;
		let count = 0;

		while (value > 0) {
			if (value & 1) {
				count++;
			}

			value >>= 1;
		}

		return count;
	}

	intersect(...masks) {
		this.value &= BitField.combineMasks(...masks);

		return this;
	}

	intersects(...masks) {
		const mask = BitField.combineMasks(...masks);

		return (this.value & mask) !== 0;
	}

	get(index) {
		if (index < 0 || index > 31) {
			throw Error('Illegal argument: parameter \'index\' is out of bounds');
		}

		return Boolean((this.value >> index) & 1);
	}

	getRange(from, to) {
		if (from < 0 || from > 31) {
			throw Error('Illegal argument: parameter \'from\' is out of bounds');
		}

		if (to < 0 || to > 31) {
			throw Error('Illegal argument: parameter \'to\' is out of bounds');
		}

		if (to <= from) {
			throw Error('Illegal argument: parameter \'to\' must be larger than parameter \'from\'');
		}

		const length = to - from;
		const mask = (1 << length) - 1;
		const bitField = new BitField(length);
		bitField.on((this.value >> from) & mask);

		return bitField;
	}

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

		return (this.value & mask) === mask;
	}

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

		return (this.value & mask) !== 0;
	}

	testAt(value, index) {
		if (index < 0 || index > 31) {
			throw Error('Illegal argument: parameter \'index\' is out of bounds');
		}

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

	testAll(value) {
		let mask = 0;

		if (value > 0) {
			mask = (value << this.length) - 1;
		}

		return this.value === mask;
	}

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

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

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

		if (value > 0) {
			this.value |= mask;
		} else {
			this.value &= ~mask;
		}

		return this;
	}

	setAll(value) {
		const mask = (1 << this.length) - 1;

		return this.set(value, mask);
	}

	setAt(value, index) {
		if (index < 0 || index > 31) {
			throw Error('Illegal argument: parameter \'index\' is out of bounds');
		}

		const mask = 1 << index;

		return this.set(value, mask);
	}

	setRange(value, from, to) {
		if (from < 0 || from > 31) {
			throw Error('Illegal argument: parameter \'from\' is out of bounds');
		}

		if (to < 0 || to > 31) {
			throw Error('Illegal argument: parameter \'to\' is out of bounds');
		}

		if (to <= from) {
			throw Error('Illegal argument: parameter \'to\' must be larger than parameter \'from\'');
		}

		let mask = (1 << (to - from)) - 1;

		if (from > 0) {
			mask *= 2 * from;
		}

		return this.set(value, mask);
	}

	flip(...masks) {
		this.value ^= BitField.combineMasks(...masks);

		return this;
	}

	flipAll() {
		const mask = (1 << this.length) - 1;

		return this.flip(mask);
	}

	flipAt(index) {
		if (index < 0 || index > 31) {
			throw Error('Illegal argument: parameter \'index\' is out of bounds');
		}

		const mask = 1 << index;

		return this.flip(mask);
	}

	flipRange(from, to) {
		if (from < 0 || from > 31) {
			throw Error('Illegal argument: parameter \'from\' is out of bounds');
		}

		if (to < 0 || to > 31) {
			throw Error('Illegal argument: parameter \'to\' is out of bounds');
		}

		if (to <= from) {
			throw Error('Illegal argument: parameter \'to\' must be larger than parameter \'from\'');
		}

		let mask = (1 << (to - from)) - 1;

		if (from > 0) {
			mask *= 2 * from;
		}

		return this.flip(mask);
	}

	copy(bitset) {
		this.value = BitField.valueOf(bitset);

		return this;
	}

	valueOf() {
		return this.value;
	}

	serialize() {
		let output = this.value.toString(2);

		if (this.minLength > output.length) {
			output = '0'.repeat(this.minLength - output.length) + output;
		}

		return output;
	}

	/**
	 * Deserializes a string and returns a new instance.
	 *
	 * @public
	 * @static
	 * @param {String} input
	 * @throws {Error} In case input could not be parsed.
	 * @returns {BitField} A new instance.
	 */
	static deserialize(input) {
		if (isNaN(Number(input))) {
			throw new Error('Failed to deserialize input');
		}

		const array = input.split('');

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

	clone() {
		return new BitField(this.minLength).copy(this);
	}

	equals(other) {
		return this.value === BitField.valueOf(other);
	}

	toArray() {
		const array = [];
		let { value } = this;
		let length = 0;

		while (value > 0) {
			length++;
			array.push(Boolean(value & 1));
			value >>= 1;
		}

		if (this.minLength > length) {
			const filler = new Array(this.minLength - length).fill(false);

			array.push(...filler);
		}

		return array.reverse();
	}

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

export default BitField;