Skip to main content

Built and signed on GitHub Actions

A Hyper-Performant GC-Minimizing UInt32Array-Backed Boolean Array in Typescript

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
3 months ago (0.6.0)
Package root>src>BooleanArray.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
/** * @module BooleanArray * @description A boolean array backed by a Uint32Array. * @copyright 2024 the BooleanArray authors. All rights reserved. * @license MIT */ /** * Performs a bitwise AND operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise AND operation */ export function and(a: number, b: number): number { return a & b; } /** * Performs a bitwise difference operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise difference operation */ export function difference(a: number, b: number): number { return a & ~b; } /** * Performs a bitwise NAND operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise NAND operation */ export function nand(a: number, b: number): number { return ~(a & b); } /** * Performs a bitwise NOR operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise NOR operation */ export function nor(a: number, b: number): number { return ~(a | b); } /** * Performs a bitwise NOT operation with a number * @param a the number to perform the bitwise NOT operation on * @returns the result of the bitwise NOT operation */ export function not(a: number): number { return ~a; } /** * Performs a bitwise OR operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise OR operation */ export function or(a: number, b: number): number { return a | b; } /** * Performs a bitwise XOR operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise XOR operation */ export function xor(a: number, b: number): number { return a ^ b; } /** * Performs a bitwise XNOR operation with two numbers * @param a the first number * @param b the second number * @returns the result of the bitwise XNOR operation */ export function xnor(a: number, b: number): number { return ~(a ^ b); } /** * Count set bits in chunk * @param value the value to count the set bits in * @returns the number of set bits in the value */ export function countChunk(value: number): number { value = value - ((value >>> 1) & 0x55555555); value = (value & 0x33333333) + ((value >>> 2) & 0x33333333); value = (value + (value >>> 4)) & 0x0f0f0f0f; return ((value * 0x01010101) >>> 24); } /** A fast boolean array backed by a Uint32Array */ export class BooleanArray extends Uint32Array { // Config /** The threshold for what is considered a large array */ static LARGE_RANGE_THRESHOLD = 1024; /** The threshold for what is considered a dense array */ static DENSE_ARRAY_THRESHOLD = 0.75; // Static values /** The number of bits per integer */ static readonly BITS_PER_INT = 32 as const; /** The mask for the chunk offset */ static readonly CHUNK_MASK = 31 as const; /** The shift for the chunk offset */ static readonly CHUNK_SHIFT = 5 as const; /** The mask for all bits (~0 >>> 0) */ static readonly ALL_BITS = 4294967295 as const; /** The maximum safe size for bit operations */ static readonly MAX_SAFE_SIZE = 536870911 as const; // Math.floor((2 ** 32 - 1) / 8); /** * Performs a bitwise AND operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static and(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, and); } /** * Checks if two BooleanArrays are equal * @param a the first BooleanArray * @param b the second BooleanArray * @returns `true` if the arrays are equal, `false` otherwise */ static equals(a: BooleanArray, b: BooleanArray): boolean { if (a.size !== b.size) { return false; } for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } /** * Performs a bitwise difference operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static difference(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, difference); } /** * Create a BooleanArray from an array of numbers, each number representing a bit to set to true. * @param arr The array of numbers to create the BooleanArray from * @param size The size of the BooleanArray * @returns A new BooleanArray instance */ static fromArray(arr: Array<number>, size: number): BooleanArray { const pool = new BooleanArray(size); if (arr.some((n) => typeof n !== "number" || isNaN(n))) { throw new TypeError("BitPool.fromArray: array contains non-number or NaN values"); } for (let i = 0; i < arr.length; i++) { pool.setBool(arr[i]!, true); } return pool; } /** * Create a BooleanArray from an object, using the object's keys as the bit indices. * @param obj The object to create the BooleanArray from * @param size The size of the BooleanArray * @returns A new BooleanArray instance */ static fromObjects<T>(size: number, key: keyof T, objs: T[]): BooleanArray { return objs.reduce((bitfield, obj) => { bitfield.setBool(obj[key] as number, true); return bitfield; }, new BooleanArray(size)); } /** * Get the chunk index for a given bool index * @param index the bool index to get the chunk index for * @returns the chunk index */ static getChunk(index: number): number { // This shifts the bits of `index` five places to the right, effectively dividing `index` by 32. // This finds the index in the array where the specified bool would be located. return index >>> BooleanArray.CHUNK_SHIFT; } /** * Get the number of chunks required to accommodate a given number of bools * @param bools the number of bools to get the chunk count for * @returns the number of chunks */ static getChunkCount(bools: number): number { return (bools + BooleanArray.BITS_PER_INT - 1) >>> BooleanArray.CHUNK_SHIFT; } /** * Get the offset of a bool within a chunk * @param boolIndex the bool index to get the offset for * @returns the offset */ static getChunkOffset(boolIndex: number): number { return boolIndex & BooleanArray.CHUNK_MASK; } /** * Performs a bitwise NAND operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static nand(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, nand); } /** * Performs a bitwise NOR operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static nor(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, nor); } /** * Performs a bitwise NOT operation with a BooleanArray * @param a the BooleanArray to perform the bitwise NOT operation on * @returns a new BooleanArray with the result */ static not(a: BooleanArray): BooleanArray { return BooleanArray.operate(a, a, not); } /** * Performs a bitwise operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @param operation the bitwise operation to perform * @returns a new BooleanArray with the result */ static operate(a: BooleanArray, b: BooleanArray, operation: (a: number, b: number) => number): BooleanArray { if (a.size !== b.size) { throw new Error("Arrays must have the same size"); } const result = new BooleanArray(a.size); for (let i = 0; i < a.length; i++) { result[i] = operation(a[i]!, b[i]!); } return result; } /** * Performs a bitwise OR operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static or(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, or); } /** * Validate a value * @param value the value to validate * @returns the validated value * @throws {TypeError} if `value` is not a safe integer * @throws {RangeError} if `value` is less than 1, or is greater than BooleanArray.MAX_SAFE_SIZE */ static validateValue(value: number, maxSize?: number): number { if (typeof value !== "number" || Number.isNaN(value) || !Number.isSafeInteger(value)) { throw new TypeError('"value" must be a safe integer.'); } if (value < 0) { throw new RangeError('"value" must be greater than or equal to 0.'); } if (value > BooleanArray.MAX_SAFE_SIZE) { throw new RangeError(`"value" must be smaller than or equal to ${BooleanArray.MAX_SAFE_SIZE}.`); } if (maxSize !== undefined && value >= maxSize) { throw new RangeError( `Index ${value} is out of bounds for array of size ${maxSize}. BooleanArrays are 0-indexed, try ${ maxSize - 1 } instead.`, ); } return value; } /** * Performs a bitwise XOR operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static xor(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, xor); } /** * Performs a bitwise XNOR operation with two BooleanArrays * @param a the first BooleanArray * @param b the second BooleanArray * @returns a new BooleanArray with the result */ static xnor(a: BooleanArray, b: BooleanArray): BooleanArray { return BooleanArray.operate(a, b, xnor); } /** The total number of bits in the array */ #size: number; /** * Creates a new BooleanArray * @param size the number of bits required in the array (min = 1, max = 4294967295) * @returns a new BooleanArray * @throws {RangeError} if `size` is less than 1, or is greater than 0xffffffff (2 ** 32 - 1) === (4294967295) * @throws {TypeError} if `size` is NaN */ constructor(size: number) { super(BooleanArray.getChunkCount(BooleanArray.validateValue(size))); this.#size = size; } /** @returns the total number of bits in the bitfield */ get size(): number { return this.#size; } /** * Clear the array * @returns `this` for chaining */ clear(): this { this.fill(0); return this; } /** * Creates a copy of this BooleanArray * @returns a new BooleanArray with the same contents */ clone(): BooleanArray { const copy = new BooleanArray(this.#size); copy.set(this); return copy; } /** * Get the boolean state of a bit * @param index the bit index to get the state of * @returns the boolean state of the bit */ getBool(index: number): boolean { BooleanArray.validateValue(index, this.#size); const chunk = BooleanArray.getChunk(index); const offset = BooleanArray.getChunkOffset(index); return (this[chunk]! & (1 << offset)) !== 0; } /** * Add bulk operations for better performance * @param startIndex the start index to get the booleans from * @param count the number of booleans to get * @returns an array of booleans * @throws {RangeError} if `startIndex` is out of bounds * @throws {RangeError} if `count` is out of bounds */ getBools(startIndex: number, count: number): boolean[] { BooleanArray.validateValue(startIndex, this.#size); BooleanArray.validateValue(count); if (startIndex + count > this.#size) { throw new RangeError("Range exceeds array bounds"); } const result: boolean[] = new Array(count); for (let i = 0; i < count; i++) { result[i] = this.getBool(startIndex + i); } return result; } /** * Get the index of the first set bit starting from a given index * @param startIndex the index to start searching from [default = 0] * @returns the index of the first set bit, or -1 if no bits are set */ getFirstSetIndex(startIndex: number = 0): number { BooleanArray.validateValue(startIndex, this.#size); const startChunk = BooleanArray.getChunk(startIndex); const startOffset = BooleanArray.getChunkOffset(startIndex); // Handle first chunk with mask for bits after startOffset const firstChunkMask = (BooleanArray.ALL_BITS << startOffset) >>> 0; const firstChunk = this[startChunk]! & firstChunkMask; if (firstChunk !== 0) { const bitPos = Math.clz32(firstChunk & -firstChunk) ^ 31; const index = (startChunk << BooleanArray.CHUNK_SHIFT) + bitPos; return index < this.#size ? index : -1; } // Search remaining chunks for (let i = startChunk + 1; i < this.length; i++) { const chunk = this[i]!; if (chunk !== 0) { const bitPos = Math.clz32(chunk & -chunk) ^ 31; const index = (i << BooleanArray.CHUNK_SHIFT) + bitPos; return index < this.#size ? index : -1; } } return -1; } /** * Get the index of the last set bit * @param startIndex the index to start searching from [default = this.size] * @returns the index of the last set bit, or -1 if no bits are set */ getLastSetIndex(startIndex: number = this.#size): number { BooleanArray.validateValue(startIndex, this.#size + 1); // Adjust startIndex to be within bounds const effectiveStart = Math.min(startIndex, this.#size); // Get the chunk containing startIndex const startChunk = BooleanArray.getChunk(effectiveStart - 1); // Handle first chunk const firstOffset = effectiveStart & BooleanArray.CHUNK_MASK; const chunk = this[startChunk]!; if (chunk !== 0) { const mask = firstOffset === 0 ? BooleanArray.ALL_BITS : ((1 << firstOffset) - 1) >>> 0; const maskedChunk = chunk & mask; if (maskedChunk !== 0) { const bitPos = 31 - Math.clz32(maskedChunk); return (startChunk << BooleanArray.CHUNK_SHIFT) + bitPos; } } // Search remaining chunks backwards for (let i = startChunk - 1; i >= 0; i--) { const chunk = this[i]!; if (chunk !== 0) { const bitPos = 31 - Math.clz32(chunk); return (i << BooleanArray.CHUNK_SHIFT) + bitPos; } } return -1; } /** * Get the number of set bits in the array * @returns the number of set bits in the array */ getPopulationCount(): number { let count = 0; const lastIndex = this.length - 1; // Count all full chunks for (let i = 0; i < lastIndex; i++) { count += countChunk(this[i]!); } // Handle last chunk const remainingBits = this.#size % BooleanArray.BITS_PER_INT; const lastChunkMask = remainingBits === 0 ? BooleanArray.ALL_BITS : ((1 << remainingBits) - 1) >>> 0; count += countChunk(this[lastIndex]! & lastChunkMask); return count; } /** * Check if the array is empty * @returns `true` if the array is empty, `false` otherwise */ isEmpty(): boolean { // Process 8 chunks at a time for large arrays const len = this.length; let i = 0; if (len >= 8) { for (; i + 7 < len; i += 8) { if ( (this[i]! | this[i + 1]! | this[i + 2]! | this[i + 3]! | this[i + 4]! | this[i + 5]! | this[i + 6]! | this[i + 7]!) !== 0 ) { return false; } } } // Handle remaining chunks for (; i < len; i++) { if (this[i] !== 0) return false; } return true; } /** * Set all bits to `true` * @returns `this` for chaining */ setAll(): this { // Fill all chunks with ALL_BITS this.fill(BooleanArray.ALL_BITS); // Mask off any excess bits in the last chunk if needed const remainingBits = this.#size % BooleanArray.BITS_PER_INT; if (remainingBits > 0) { const lastIndex = this.length - 1; const mask = ((1 << remainingBits) - 1) >>> 0; this[lastIndex] = mask; } return this; } /** * Set the boolean state of a bit * @param index the bit index to set the state of * @param value the boolean state to set the bit to * @returns `this` for chaining */ setBool(index: number, value: boolean): this { BooleanArray.validateValue(index, this.#size); const chunk = BooleanArray.getChunk(index); const offset = BooleanArray.getChunkOffset(index); const mask = 1 << offset; if (value) { this[chunk]! |= mask; } else { this[chunk]! &= ~mask; } return this; } /** * Set a range of bits to a given value * @param startIndex the start index to set the range from * @param count the number of booleans to set * @param value the boolean value to set * @returns `this` for chaining * @throws {RangeError} if `startIndex` is out of bounds * @throws {RangeError} if `count` is out of bounds */ setRange(startIndex: number, count: number, value: boolean): this { BooleanArray.validateValue(startIndex, this.#size); BooleanArray.validateValue(count); if (startIndex + count > this.#size) { throw new RangeError("Range exceeds array bounds"); } // Fast path for large ranges if (count > BooleanArray.LARGE_RANGE_THRESHOLD) { const fillValue = value ? BooleanArray.ALL_BITS : 0; this.fill(fillValue, startIndex >>> 5, (startIndex + count + 31) >>> 5); return this; } // Fast path for small ranges that fit in a single chunk if (count <= 32 && BooleanArray.getChunk(startIndex) === BooleanArray.getChunk(startIndex + count - 1)) { const chunk = BooleanArray.getChunk(startIndex); const startOffset = BooleanArray.getChunkOffset(startIndex); const mask = ((1 << count) - 1) << startOffset; this[chunk] = value ? (this[chunk]! | mask) : (this[chunk]! & ~mask); return this; } const startChunk = BooleanArray.getChunk(startIndex); const endChunk = BooleanArray.getChunk(startIndex + count - 1); // Handle first chunk const startOffset = BooleanArray.getChunkOffset(startIndex); const firstChunkMask = (BooleanArray.ALL_BITS << startOffset) >>> 0; this[startChunk] = value ? (this[startChunk]! | firstChunkMask) : (this[startChunk]! & ~firstChunkMask); // Fill complete chunks for (let i = startChunk + 1; i < endChunk; i++) { this[i] = value ? BooleanArray.ALL_BITS : 0; } // Handle last chunk if different from first chunk if (startChunk !== endChunk) { const remainingBits = (startIndex + count) & BooleanArray.CHUNK_MASK; const lastChunkMask = remainingBits === 0 ? BooleanArray.ALL_BITS : ((1 << remainingBits) - 1) >>> 0; this[endChunk] = value ? (this[endChunk]! | lastChunkMask) : (this[endChunk]! & ~lastChunkMask); } return this; } /** * Toggle the boolean state of a bit * @param index the bit index to toggle the state of * @returns the new boolean state of the bit */ toggleBool(index: number): boolean { BooleanArray.validateValue(index, this.#size); const chunk = BooleanArray.getChunk(index); const offset = BooleanArray.getChunkOffset(index); const mask = 1 << offset; this[chunk]! ^= mask; return (this[chunk]! & mask) !== 0; } /** * Get the indices of the set bits in the array * @param startIndex the start index to get the indices from [default = 0] * @param endIndex the end index to get the indices from [default = this.size] * @returns Iterator of indices where bits are set */ *truthyIndices(startIndex: number = 0, endIndex: number = this.#size): IterableIterator<number> { BooleanArray.validateValue(startIndex, this.#size); BooleanArray.validateValue(endIndex, this.#size + 1); if (startIndex >= endIndex) return; const len = this.length; let i = BooleanArray.getChunk(startIndex); // Fast path for dense arrays (>75% bits set) if (this.getPopulationCount() > (this.#size * BooleanArray.DENSE_ARRAY_THRESHOLD)) { for (let index = startIndex; index < endIndex; index++) { if (this.getBool(index)) yield index; } return; } // Process chunks of 8 for better vectorization for (; i + 7 < len; i += 8) { for (let j = 0; j < 8; j++) { const chunk = this[i + j]!; if (chunk === 0) continue; const baseIndex = (i + j) << BooleanArray.CHUNK_SHIFT; if (chunk === BooleanArray.ALL_BITS) { for (let k = 0; k < BooleanArray.BITS_PER_INT; k++) { const index = baseIndex + k; if (index >= endIndex) return; if (index >= startIndex) yield index; } continue; } let remaining = chunk; while (remaining !== 0) { const trailingZeros = Math.clz32((remaining & -remaining) >>> 0) ^ BooleanArray.CHUNK_MASK; const index = baseIndex + trailingZeros; if (index >= endIndex) return; if (index >= startIndex) yield index; remaining &= remaining - 1; } } } // Handle remaining chunks for (; i < len; i++) { const chunk = this[i]!; if (chunk === 0) continue; const baseIndex = i << BooleanArray.CHUNK_SHIFT; let remaining = chunk; while (remaining !== 0) { const trailingZeros = Math.clz32((remaining & -remaining) >>> 0) ^ BooleanArray.CHUNK_MASK; const index = baseIndex + trailingZeros; if (index >= endIndex) return; if (index >= startIndex) yield index; remaining &= remaining - 1; } } } }