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 { 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; } }