| [ Web Proxy ] |
| Viewing: https://raw.org/article/javascript-bit-array/ | [Back] [Original] |
BitSet.js is a compact bit-vector library for JavaScript. It stores bits in an array of 32-bit words, grows beyond the range of native bitwise operators, and preserves an infinite run of leading ones after a complement. The package works in Node.js and browsers and includes CommonJS, ESM, and TypeScript declarations.
JavaScript numbers are normally IEEE-754 double-precision values, but the ordinary bitwise operators first convert their operands to signed 32-bit integers. All 32 bits participate in the operation; the sign bit is not lost. The practical limitation is that the result is still a fixed-width signed integer, and shift counts are reduced modulo 32:
1 << 8; // 256
1 << 40; // 256 again, because 40 modulo 32 is 8
1 << 31; // -2147483648 That behavior is useful for compact flags, but it cannot address bit 40, 128, or 10,000 independently. BitSet.js maps index i to word i >>> 5 and bit i & 31. A high index therefore selects another 32-bit word instead of wrapping into the first one.
npm install bitset
The npm package is named bitset. Version 5.2.3 provides equivalent ESM and CommonJS entry points:
import BitSet from 'bitset';
const bits = new BitSet(); const BitSet = require('bitset');
const bits = new BitSet(); The browser build is available as dist/bitset.min.js in the package and exposes the global BitSet constructor.
Indexes start at zero. set() defaults to one and returns the same object, so several mutations can be chained:
const flags = new BitSet()
.set(0)
.set(31)
.set(32)
.set(128);
console.log(flags.get(128)); // 1
console.log(flags.cardinality()); // 4
console.log(flags.toArray()); // [0, 31, 32, 128] Setting a bit to zero is equivalent to clearing it:
flags.set(31, 0);
flags.clear(32);
console.log(flags.toArray()); // [0, 128] setRange(from, to, value), clear(from, to), and flip(from, to) use inclusive endpoints. Omitting both arguments from clear() empties the set; omitting them from flip() complements every bit, including the infinite leading region.
const window = new BitSet();
window.setRange(10, 18);
window.clear(14);
window.flip(18);
console.log(window.toArray());
// [10, 11, 12, 13, 15, 16, 17] The constructor accepts binary or hexadecimal strings, an array of set indexes, a byte array, a native number, or another BitSet:
new BitSet('101001'); // Binary string
new BitSet('0b101001'); // Explicit binary string
new BitSet('0x29'); // Hexadecimal string
new BitSet([0, 3, 5, 128]); // Set these indexes
new BitSet(new Uint8Array([0x29]));
new BitSet(41); // The numeric value 41
new BitSet(existingBits); // Clone another BitSet The distinction between a numeric value and an index is important. new BitSet(128) represents the number 128 and therefore sets bit 7. To set bit 128, use new BitSet([128]) or new BitSet().set(128).
Bitwise operations correspond directly to set operations. Unlike the mutation methods above, these methods return a new BitSet and leave both operands unchanged:
| Method | Bit operation | Set interpretation |
|---|---|---|
a.and(b) | AND | Intersection |
a.or(b) | OR | Union |
a.xor(b) | XOR | Symmetric difference |
a.andNot(b) | AND NOT | Difference |
a.not() | NOT | Complement |
const left = new BitSet([1, 3, 5]);
const right = new BitSet([2, 3, 4]);
console.log(left.and(right).toArray()); // [3]
console.log(left.or(right).toArray()); // [1, 2, 3, 4, 5]
console.log(left.xor(right).toArray()); // [1, 2, 4, 5]
console.log(left.andNot(right).toArray()); // [1, 5]
console.log(left.toArray()); // Still [1, 3, 5] A normal finite array cannot distinguish between "all unallocated high bits are zero" and "all unallocated high bits are one." BitSet.js stores that state separately. Complementing a finite set flips its stored words and changes the leading-bit state to one:
const selected = new BitSet([0, 2]);
const excluded = selected.not();
console.log(excluded.get(0)); // 0
console.log(excluded.get(1)); // 1
console.log(excluded.get(10000)); // 1
console.log(excluded.cardinality()); // Infinity
console.log(excluded.toString()); // Begins with "...1111" This is a genuine infinite set of non-negative bit indexes, not merely a finite word mask. Consequently, cardinality() and msb() return Infinity. The iterator over an inverted set also never terminates unless the caller stops it. Avoid spreading such a set into an array or using an unbounded for...of loop.
| Method | Result |
|---|---|
get(index) | Bit value 0 or 1 |
cardinality() | Number of set bits |
msb() | Highest set index |
lsb() | Lowest set index |
ntz() | Number of trailing zero bits |
isEmpty() | Whether every bit is zero |
equals(other) | Exact bitwise equality |
toArray() | Set indexes for a finite set |
toString(base) | Representation in base 2 through 36 |
slice(from, to) creates a new zero-based BitSet from an inclusive source range. Both fromBinaryString() and fromHexString() are explicit alternatives to prefixed strings, and Random(n) creates at most n random bits. For an empty set, msb() and ntz() return Infinity.
const packetFlags = BitSet.fromHexString('a501');
const lowByte = packetFlags.slice(0, 7);
console.log(lowByte.toString(16)); // 1
console.log(packetFlags.toString(2));
// 1010010100000001 The default iterator yields each bit value from index zero through the highest meaningful bit; it does not yield only the set indexes:
console.log([...new BitSet('101')]); // [1, 0, 1] A bit vector is useful when every permission has a stable integer index and the set is dense enough to justify a compact bitmap:
const permission = {
read: 0,
write: 1,
exportData: 75,
};
const editor = new BitSet([
permission.read,
permission.write,
]);
const exporter = editor.clone().set(permission.exportData);
function can(role, capability) {
return role.get(capability) === 1;
}
console.log(can(editor, permission.exportData)); // false
console.log(can(exporter, permission.exportData)); // true Persist a finite set as a binary or hexadecimal string when the storage layer must preserve indexes beyond its native integer width. Keep a stable mapping from permission names to indexes, and never interpret a client-supplied bitmap as authorization without checking it against server-side policy.
Bit vectors also represent dense membership tables efficiently. Here a set bit means "composite":
function primesUpTo(limit) {
const composite = new BitSet();
for (let candidate = 2; candidate * candidate <= limit; candidate++) {
if (composite.get(candidate) === 0) {
for (
let multiple = candidate * candidate;
multiple <= limit;
multiple += candidate
) {
composite.set(multiple);
}
}
}
const primes = [];
for (let value = 2; value <= limit; value++) {
if (composite.get(value) === 0) primes.push(value);
}
return primes;
}
console.log(primesUpTo(30));
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] The same dense-membership idea appears in a Bloom filter, where several hash functions map values to bit positions and membership is probabilistic.
BitSet.js is a dense bit vector. Access to an already allocated word is constant time, while first setting a distant index must grow and initialize every intervening 32-bit word. Storage through index n is therefore proportional to n / 32, and whole-set Boolean operations are proportional to the larger allocated word count. A sparse set of a few enormous indexes is better represented by a Set or another sparse structure.
The implementation coerces indexes through JavaScript's 32-bit integer operations, so use non-negative integer indexes and avoid values near the language and array limits. BitSet.js is not a replacement for BigInt arithmetic: it models independently addressable bits and set operations, while BigInt models an integer value with arithmetic operators.
| Web Proxy Viewer | New URL | Original Page |