aesy/Easy-Bits

View on GitHub
src/BitArray.js

Summary

Maintainability
A
3 hrs
Test Coverage
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;