src/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;