bitmap-index/src/bitmap.ts

387 lines
11 KiB
TypeScript

const computeBitsArrayLength = (x: number): number => Math.ceil(x / 8);
/**
* Compute the number of bytes required to store `x` bits.
* @private
* @param {number} x - Number of bits to accommodate.
* @returns {number} Number of bytes required.
*/
/**
* Get the byte index that contains the bit for position `x`.
* @private
* @param {number} x - Bit index.
* @returns {number} Byte index in the underlying Uint8Array.
*/
const getBitPositionIndex = (x: number): number => Math.floor(x / 8);
/**
* Get the position of the bit inside its byte for bit index `x`.
* @private
* @param {number} x - Bit index.
* @returns {number} Bit position within the byte (0-7).
*/
const getBitPosition = (x: number): number => x % 8;
/**
* Create an 8-bit mask with a 1 at `bitIdx`.
* @private
* @param {number} bitIdx - Bit position inside a byte (0-7).
* @returns {number} Mask with the bit at `bitIdx` set (e.g. 0b00001000).
*/
const getMask = (bitIdx: number): number => 0b00000001 << bitIdx;
export class Bitmap {
private bits: Uint8Array;
/**
* Bitmap - a simple bitset backed by a Uint8Array.
* Indices are zero-based.
* @example
* const b = new Bitmap(64); // capacity for 64 bits
* b.set(10);
* b.set(20);
* console.log(b.contains(10)); // true
*
* @param {number} [x=32] - Initial capacity in bits (defaults to 32).
*/
constructor(x: number = 32) {
this.bits = new Uint8Array(computeBitsArrayLength(x));
}
/**
* Iterate over all set bit indices in ascending order.
* Yields the numeric index of each set bit.
* @yields {number} The index of a set bit (0-based).
*/
public *[Symbol.iterator](): Generator<number> {
for (let i = 0; i < this.bits.length; i++) {
const byte = this.bits[i];
if (byte === 0) continue;
for (let j = 0; j < 8; j++) {
if ((byte & (1 << j)) !== 0) {
yield i * 8 + j;
}
}
}
}
/**
* Serialize the bitmap into a compact binary-string representation.
* Each byte of the underlying storage is converted to 8 characters ('0'/'1'),
* preserving byte order and padding leading zeros to maintain alignment.
*
* @returns {string} Binary string representing the full contents of the bitmap.
*/
public toString(): string {
return Array.from(this.bits)
.map((x) => x.toString(2).padStart(8, "0"))
.join(" ");
}
/**
* Construct a Bitmap instance from its binary-string representation.
* The input string must be byte-aligned: its length must be divisible by 8.
* Each 8-character segment is parsed as a single byte (big-endian bit order).
*
* @param {string} s - Binary string produced by `toString()` or compatible formatter.
* @returns {Bitmap} New Bitmap instance containing the parsed bits.
* @throws {Error} If the input length is not divisible by 8.
*/
static fromString(s: string): Bitmap {
// allow "00000001 00000010", "0000000100000010", etc.
const cleaned = s.replace(/\s+/g, "");
if (cleaned.length % 8 !== 0) {
throw new Error("Bitmap string must be aligned to bytes (len % 8 == 0)");
}
const bytes = new Uint8Array(cleaned.length / 8);
for (let i = 0; i < bytes.length; i++) {
const byteStr = cleaned.slice(i * 8, i * 8 + 8);
bytes[i] = parseInt(byteStr, 2);
}
const b = new Bitmap(bytes.length * 8);
b.bits = bytes;
return b;
}
/**
* Ensure the underlying array can hold at least up to bit index `x`.
* This may reallocate the underlying Uint8Array, keeping existing bits.
* @param {number} x - Bit index to accommodate.
* @returns {void}
*/
public grow(x: number): void {
const arrLength = Math.max(1, computeBitsArrayLength(x + 1));
if (arrLength <= this.bits.length) return;
const prev = this.bits;
this.bits = new Uint8Array(arrLength);
this.bits.set(prev);
}
/**
* Set the bit at index `x` to 1.
* Automatically grows the bitmap if necessary.
* **Mutates this bitmap** in-place.
* @param {number} x - Bit index (0-based).
* @returns {this} The instance itself (for method chaining).
*/
public set(x: number): this {
const idx = getBitPositionIndex(x);
if (idx >= this.bits.length) this.grow(x);
this.bits[idx] |= getMask(getBitPosition(x));
return this;
}
/**
* Clear the bit at index `x` (set to 0).
* No-op if the index is outside current capacity.
* **Mutates this bitmap** in-place.
* @param {number} x - Bit index (0-based).
* @returns {this} The instance itself (for method chaining).
*/
public remove(x: number): this {
const idx = getBitPositionIndex(x);
if (idx >= this.bits.length) return this;
this.bits[idx] &= ~getMask(getBitPosition(x));
return this;
}
/**
* Check whether the bit at index `x` is set.
* @param {number} x - Bit index to test (0-based).
* @returns {boolean} True if the bit is 1, false otherwise.
*/
public contains(x: number): boolean {
const idx = getBitPositionIndex(x);
if (idx >= this.bits.length) return false;
const mask = getMask(getBitPosition(x));
return (this.bits[idx] & mask) === mask;
}
/**
* Count number of set bits in the bitmap (Hamming weight).
* @returns {number} Number of bits set to 1.
*/
public count(): number {
let count = 0;
for (const byte of this.bits) {
let tmp = byte;
while (tmp) {
tmp &= tmp - 1;
count++;
}
}
return count;
}
/**
* Bitwise AND with another bitmap.
* **Mutates this bitmap** — performs the operation in-place and clears extra bytes if needed.
* @param {Bitmap} bitmap - Other bitmap to AND with.
* @returns {this} The instance itself (for method chaining).
*/
public and(bitmap: Bitmap): this {
const other = bitmap.bits;
const minlen = Math.min(this.bits.length, other.length);
for (let i = 0; i < minlen; i++) {
this.bits[i] &= other[i];
}
for (let i = minlen; i < this.bits.length; i++) {
this.bits[i] = 0;
}
return this;
}
/**
* Bitwise AND NOT (this = this & ~bitmap).
* **Mutates this bitmap** in-place.
* @param {Bitmap} bitmap - Bitmap whose set bits will be subtracted.
* @returns {this} The instance itself (for method chaining).
*/
public andNot(bitmap: Bitmap): this {
const other = bitmap.bits;
const minlen = Math.min(this.bits.length, other.length);
for (let i = 0; i < minlen; i++) {
this.bits[i] &= ~other[i];
}
return this;
}
/**
* Bitwise OR (union) with another bitmap.
* **Mutates this bitmap** in-place and grows it if necessary.
* @param {Bitmap} bitmap - Other bitmap to OR with.
* @returns {this} The instance itself (for method chaining).
*/
public or(bitmap: Bitmap): this {
const other = bitmap.bits;
if (other.length > this.bits.length) {
const newArr = new Uint8Array(other.length);
newArr.set(this.bits);
this.bits = newArr;
}
const maxlen = Math.max(this.bits.length, other.length);
for (let i = 0; i < maxlen; i++) {
const a = this.bits[i] ?? 0;
const b = other[i] ?? 0;
this.bits[i] = a | b;
}
return this;
}
/**
* Bitwise XOR with another bitmap.
* **Mutates this bitmap** in-place and grows it if necessary.
* @param {Bitmap} bitmap - Other bitmap to XOR with.
* @returns {this} The instance itself (for method chaining).
*/
public xor(bitmap: Bitmap): this {
const other = bitmap.bits;
if (other.length > this.bits.length) {
const newArr = new Uint8Array(other.length);
newArr.set(this.bits);
this.bits = newArr;
}
const maxlen = Math.max(this.bits.length, other.length);
for (let i = 0; i < maxlen; i++) {
const a = this.bits[i] ?? 0;
const b = other[i] ?? 0;
this.bits[i] = a ^ b;
}
return this;
}
/**
* Iterate over set bits and call `fn` for each set bit index in ascending order.
* If `fn` returns `false`, iteration stops early.
* @param {(x: number) => boolean | void} fn - Callback invoked with each set bit index. Returning `false` stops iteration.
* @returns {void}
*/
public range(fn: (x: number) => boolean | void): void {
let needContinueIterating = true;
for (let i = 0; i < this.bits.length; i++) {
let byte = this.bits[i];
for (let j = 0; j < 8; j++) {
const bit = (byte >> j) & 1;
if (bit === 0) continue;
needContinueIterating = fn(i * 8 + j) ?? true;
if (!needContinueIterating) break;
}
if (!needContinueIterating) break;
}
}
/**
* Remove bits for which the predicate returns `false`.
* **Mutates this bitmap** in-place.
* @param {(bitIndex: number) => boolean} fn - Predicate; bit is cleared if `false` is returned.
* @returns {void}
*/
public filter(fn: (x: number) => boolean): void {
for (let i = 0; i < this.bits.length; i++) {
for (let j = 0; j < 8; j++) {
const bitIndex = i * 8 + j;
if (this.contains(bitIndex) && !fn(bitIndex)) {
this.remove(bitIndex);
}
}
}
}
/**
* Clear all bits in the bitmap (set everything to 0).
* **Mutates this bitmap** in-place.
* @returns {void}
*/
public clear(): void {
for (let i = 0; i < this.bits.length; i++) {
this.bits[i] = 0;
}
}
/**
* Create a deep copy of this bitmap.
* The clone's capacity is equal to the number of bits represented by the current byte-length.
* @returns {Bitmap} A new Bitmap instance with the same bits set.
*/
public clone(): Bitmap {
const clonedBitmap = new Bitmap(this.bits.length * 8);
clonedBitmap.bits.set(this.bits);
return clonedBitmap;
}
/**
* Return the smallest set bit index or -1 if none are set.
* @returns {number} Minimum set bit index, or -1 if empty.
*/
public min(): number {
for (let i = 0; i < this.bits.length; i++) {
const byte = this.bits[i];
if (byte === 0) continue;
for (let j = 0; j < 8; j++) {
if ((byte & (1 << j)) !== 0) {
return i * 8 + j;
}
}
}
return -1;
}
/**
* Return the largest set bit index or -1 if none are set.
* @returns {number} Maximum set bit index, or -1 if empty.
*/
public max(): number {
let x = -1;
for (let i = this.bits.length - 1; i >= 0; i--) {
const byte = this.bits[i];
if (byte === 0) continue;
x = i * 8 + Math.floor(Math.log2(byte));
break;
}
return x;
}
/**
* Return the first zero bit index (smallest index not set). If all bits are set, returns capacity (bits.length * 8).
* @returns {number} Index of the first zero bit, or capacity if none found.
*/
public minZero(): number {
for (let i = 0; i < this.bits.length; i++) {
const byte = this.bits[i];
if (byte === 0xff) continue;
for (let j = 0; j < 8; j++) {
if ((byte & (1 << j)) === 0) {
return i * 8 + j;
}
}
}
return this.bits.length * 8;
}
/**
* Return the last zero bit index (largest index not set) or -1 if all bits are set.
* @returns {number} Index of the last zero bit, or -1 if none found.
*/
public maxZero(): number {
for (let i = this.bits.length - 1; i >= 0; i--) {
const byte = this.bits[i];
if (byte === 0xff) continue;
for (let j = 7; j >= 0; j--) {
if ((byte & (1 << j)) === 0) {
return i * 8 + j;
}
}
}
return -1;
}
}