feat(stdlib): new modules + eslint/tsconfig migration
- Add array/async/etc. modules and type tests; migrate to eslint flat config and composite tsconfig (vitest typecheck enabled). - Fix PubSub.emit to snapshot listeners before iterating (stable EventEmitter semantics; avoids invoking listeners added during the same emit).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { flagsGenerator } from '.';
|
||||
|
||||
describe('flagsGenerator', () => {
|
||||
@@ -23,4 +23,17 @@ describe('flagsGenerator', () => {
|
||||
|
||||
expect(() => generateFlag()).toThrow(new RangeError('Cannot create more than 31 flags'));
|
||||
});
|
||||
|
||||
it('produce 31 distinct, orthogonal powers of two up to 2^30', () => {
|
||||
const generateFlag = flagsGenerator();
|
||||
const flags = Array.from({ length: 31 }, () => generateFlag());
|
||||
|
||||
expect(new Set(flags).size).toBe(31);
|
||||
flags.forEach((flag, i) => {
|
||||
expect(flag).toBe(2 ** i);
|
||||
expect(flag & (flag - 1)).toBe(0); // exactly one bit set
|
||||
});
|
||||
|
||||
expect(flags.at(-1)).toBe(2 ** 30);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { and, or, not, has, is, unset, toggle } from '.';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { and, has, is, not, or, toggle, unset } from '.';
|
||||
|
||||
describe('flagsAnd', () => {
|
||||
it('no effect on zero flags', () => {
|
||||
@@ -61,6 +61,15 @@ describe('flagsHas', () => {
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('require ALL queried bits, not just any (partial overlap is false)', () => {
|
||||
// 0b1000 is set but 0b0100 is not — partial overlap must be false
|
||||
expect(has(0b1010, 0b1100)).toBe(false);
|
||||
// both bits present
|
||||
expect(has(0b1110, 0b1100)).toBe(true);
|
||||
// querying zero bits is vacuously true
|
||||
expect(has(0b1010, 0b0000)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsIs', () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ export function or(...flags: number[]) {
|
||||
/**
|
||||
* @name not
|
||||
* @category Bits
|
||||
* @description Function to combine multiple flags using the XOR operator
|
||||
* @description Function to apply the bitwise NOT (complement) operator to a flag
|
||||
*
|
||||
* @param {number} flag - The flag to apply the NOT operator to
|
||||
* @returns {number} The result of the NOT operator
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './flags';
|
||||
export * from './helpers';
|
||||
export * from './vector';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { BitVector } from '.';
|
||||
|
||||
describe('BitVector', () => {
|
||||
@@ -54,10 +54,76 @@ describe('BitVector', () => {
|
||||
expect(bitVector.previousBit(0)).toBe(-1);
|
||||
});
|
||||
|
||||
it('throw RangeError when previousBit is called with an unreachable value', () => {
|
||||
it('clamp an out-of-range start index and return the previous set bit', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
bitVector.setBit(5);
|
||||
|
||||
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
|
||||
expect(bitVector.previousBit(24)).toBe(5);
|
||||
});
|
||||
|
||||
it('return -1 from previousBit on an empty out-of-range query', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
|
||||
expect(bitVector.previousBit(24)).toBe(-1);
|
||||
});
|
||||
|
||||
it('toggle bits correctly', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
|
||||
bitVector.toggleBit(7);
|
||||
expect(bitVector.getBit(7)).toBe(true);
|
||||
|
||||
bitVector.toggleBit(7);
|
||||
expect(bitVector.getBit(7)).toBe(false);
|
||||
});
|
||||
|
||||
it('find the next bit correctly', () => {
|
||||
const bitVector = new BitVector(100);
|
||||
const indices = [0, 1, 14, 15, 63, 64, 65, 66, 88, 99];
|
||||
const result = [];
|
||||
indices.forEach(index => bitVector.setBit(index));
|
||||
|
||||
for (let i = bitVector.nextBit(-1); i !== -1; i = bitVector.nextBit(i)) {
|
||||
result.push(i);
|
||||
}
|
||||
|
||||
expect(result).toEqual(indices);
|
||||
});
|
||||
|
||||
it('return -1 when no next bit is found', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
|
||||
expect(bitVector.nextBit(0)).toBe(-1);
|
||||
expect(bitVector.nextBit(15)).toBe(-1);
|
||||
});
|
||||
|
||||
it('count the number of set bits', () => {
|
||||
const bitVector = new BitVector(100);
|
||||
|
||||
expect(bitVector.count()).toBe(0);
|
||||
|
||||
[0, 5, 63, 64, 99].forEach(index => bitVector.setBit(index));
|
||||
|
||||
expect(bitVector.count()).toBe(5);
|
||||
|
||||
bitVector.clearBit(5);
|
||||
|
||||
expect(bitVector.count()).toBe(4);
|
||||
});
|
||||
|
||||
it('tolerate out-of-bounds writes without crashing or corrupting in-range bits', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
bitVector.setBit(3);
|
||||
|
||||
expect(() => {
|
||||
bitVector.setBit(1000);
|
||||
bitVector.clearBit(1000);
|
||||
bitVector.toggleBit(1000);
|
||||
}).not.toThrow();
|
||||
|
||||
// out-of-range reads are false; in-range state is intact
|
||||
expect(bitVector.getBit(1000)).toBe(false);
|
||||
expect(bitVector.getBit(3)).toBe(true);
|
||||
expect(bitVector.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ export interface BitVectorLike {
|
||||
getBit(index: number): boolean;
|
||||
setBit(index: number): void;
|
||||
clearBit(index: number): void;
|
||||
toggleBit(index: number): void;
|
||||
previousBit(index: number): number;
|
||||
nextBit(index: number): number;
|
||||
count(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +33,18 @@ export class BitVector extends Uint8Array implements BitVectorLike {
|
||||
this[index >> 3]! &= ~(1 << (index & 7));
|
||||
}
|
||||
|
||||
toggleBit(index: number): void {
|
||||
this[index >> 3]! ^= 1 << (index & 7);
|
||||
}
|
||||
|
||||
previousBit(index: number): number {
|
||||
// Clamp an out-of-range start to the vector's bit length so a query past the end
|
||||
// returns the last set bit (or -1) instead of falling through to the invariant throw.
|
||||
const totalBits = this.length << 3;
|
||||
|
||||
if (index > totalBits)
|
||||
index = totalBits;
|
||||
|
||||
while (index !== ((index >> 3) << 3)) {
|
||||
--index;
|
||||
|
||||
@@ -58,4 +72,61 @@ export class BitVector extends Uint8Array implements BitVectorLike {
|
||||
|
||||
throw new RangeError('Unreachable value');
|
||||
}
|
||||
|
||||
nextBit(index: number): number {
|
||||
const totalBits = this.length << 3;
|
||||
|
||||
let i = index + 1;
|
||||
|
||||
if (i < 0)
|
||||
i = 0;
|
||||
|
||||
// Finish scanning the remainder of the starting byte.
|
||||
while (i < totalBits && (i & 7) !== 0) {
|
||||
if (this.getBit(i))
|
||||
return i;
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
// Skip over fully-empty bytes.
|
||||
let byteIndex = i >> 3;
|
||||
|
||||
while (byteIndex < this.length && this[byteIndex] === 0)
|
||||
++byteIndex;
|
||||
|
||||
if (byteIndex >= this.length)
|
||||
return -1;
|
||||
|
||||
i = byteIndex << 3;
|
||||
|
||||
const end = i + 8;
|
||||
|
||||
while (i < end) {
|
||||
if (this.getBit(i))
|
||||
return i;
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
throw new RangeError('Unreachable value');
|
||||
}
|
||||
|
||||
count(): number {
|
||||
let total = 0;
|
||||
const len = this.length;
|
||||
|
||||
// Indexed loop — the typed-array iterator protocol (for...of) is ~3.5x slower here.
|
||||
for (let i = 0; i < len; i++) {
|
||||
// Brian Kernighan's algorithm: iterate once per set bit.
|
||||
let byte = this[i]!;
|
||||
|
||||
while (byte !== 0) {
|
||||
byte &= byte - 1;
|
||||
++total;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user