thi-ng/umbrella

View on GitHub
packages/sparse-set/src/index.ts

Summary

Maintainability
A
25 mins
Test Coverage
import type { Fn3, IEquiv, Pair, UIntArray } from "@thi.ng/api";
import type { IEquivSet } from "@thi.ng/associative/api";
import { dissoc } from "@thi.ng/associative/dissoc";
import { __inspectable } from "@thi.ng/associative/internal/inspect";
import { into } from "@thi.ng/associative/into";
import { isNumber } from "@thi.ng/checks/is-number";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";

interface SparseSetProps {
    dense: UIntArray;
    sparse: UIntArray;
    n: number;
}

/** @internal */
const __private = new WeakMap<ASparseSet<any>, SparseSetProps>();

/** @internal */
const __fail = () => illegalArgs(`dense & sparse arrays must be of same size`);

/**
 * After "An Efficient Representation for Sparse Sets" Preston Briggs and Linda
 * Torczon (1993)
 *
 * - https://research.swtch.com/sparse
 * - https://programmingpraxis.com/2012/03/09/sparse-sets/
 * - https://blog.molecular-matters.com/2013/07/24/adventures-in-data-oriented-design-part-3c-external-references/
 */
@__inspectable
export abstract class ASparseSet<T extends UIntArray>
    extends Set<number>
    implements IEquiv
{
    protected constructor(dense: T, sparse: T) {
        super();
        __private.set(this, { dense, sparse, n: 0 });
    }

    [Symbol.iterator]() {
        return this.keys();
    }

    get size(): number {
        return __private.get(this)!.n;
    }

    get capacity(): number {
        return __private.get(this)!.dense.length;
    }

    clear() {
        __private.get(this)!.n = 0;
    }

    equiv(o: any) {
        if (this === o) {
            return true;
        }
        if (!(o instanceof Set) || this.size !== o.size) {
            return false;
        }
        const $this = __private.get(this)!;
        const d = $this.dense;
        for (let i = $this.n; i-- > 0; ) {
            if (!o.has(d[i])) {
                return false;
            }
        }
        return true;
    }

    add(key: number) {
        const $this = __private.get(this)!;
        const { dense, sparse, n } = $this;
        const max = dense.length;
        const i = sparse[key];
        if (key < max && n < max && !(i < n && dense[i] === key)) {
            dense[n] = key;
            sparse[key] = n;
            $this.n++;
        }
        return this;
    }

    delete(key: number) {
        const $this = __private.get(this)!;
        const { dense, sparse } = $this;
        const i = sparse[key];
        if (i < $this.n && dense[i] === key) {
            const j = dense[--$this.n];
            dense[i] = j;
            sparse[j] = i;
            return true;
        }
        return false;
    }

    has(key: number): boolean {
        const $this = __private.get(this)!;
        const i = $this.sparse[key];
        return i < $this.n && $this.dense[i] === key;
    }

    get(key: number, notFound = -1) {
        return this.has(key) ? key : notFound;
    }

    first() {
        const $this = __private.get(this)!;
        return $this.n ? $this.dense[0] : undefined;
    }

    into(keys: Iterable<number>) {
        return <this>into(this, keys);
    }

    disj(keys: Iterable<number>) {
        return <this>dissoc(this, keys);
    }

    forEach(fn: Fn3<number, number, Set<number>, void>, thisArg?: any) {
        const $this = __private.get(this)!;
        const d = $this.dense;
        const n = $this.n;
        for (let i = 0; i < n; i++) {
            const v = d[i];
            fn.call(thisArg, v, v, this);
        }
    }

    *entries(): IterableIterator<Pair<number, number>> {
        const { dense, n } = __private.get(this)!;
        for (let i = 0; i < n; i++) {
            yield [dense[i], dense[i]];
        }
    }

    *keys(): IterableIterator<number> {
        const { dense, n } = __private.get(this)!;
        for (let i = 0; i < n; i++) {
            yield dense[i];
        }
    }

    values() {
        return this.keys();
    }

    protected __copyTo<S extends ASparseSet<T>>(dest: S) {
        const $this = __private.get(this)!;
        const $c = __private.get(dest)!;
        $c.dense = $this.dense.slice();
        $c.sparse = $this.sparse.slice();
        $c.n = $this.n;
        return dest;
    }
}

export class SparseSet8
    extends ASparseSet<Uint8Array>
    implements IEquivSet<number>
{
    constructor(dense: Uint8Array, sparse: Uint8Array);
    constructor(n: number);
    constructor(n: number | Uint8Array, sparse?: Uint8Array) {
        isNumber(n)
            ? super(new Uint8Array(n), new Uint8Array(n))
            : n.length === sparse!.length
            ? super(n, sparse!)
            : __fail();
    }

    get [Symbol.species]() {
        return SparseSet8;
    }

    get [Symbol.toStringTag]() {
        return "SparseSet8";
    }

    copy() {
        return this.__copyTo(new SparseSet8(0));
    }

    empty() {
        return new SparseSet8(this.capacity);
    }
}

export class SparseSet16
    extends ASparseSet<Uint16Array>
    implements IEquivSet<number>
{
    constructor(dense: Uint16Array, sparse: Uint16Array);
    constructor(n: number);
    constructor(n: number | Uint16Array, sparse?: Uint16Array) {
        isNumber(n)
            ? super(new Uint16Array(n), new Uint16Array(n))
            : n.length === sparse!.length
            ? super(n, sparse!)
            : __fail();
    }

    get [Symbol.species]() {
        return SparseSet16;
    }

    get [Symbol.toStringTag]() {
        return "SparseSet16";
    }

    copy() {
        return this.__copyTo(new SparseSet16(0));
    }

    empty() {
        return new SparseSet16(this.capacity);
    }
}

export class SparseSet32
    extends ASparseSet<Uint32Array>
    implements IEquivSet<number>
{
    constructor(dense: Uint32Array, sparse: Uint32Array);
    constructor(n: number);
    constructor(n: number | Uint32Array, sparse?: Uint32Array) {
        isNumber(n)
            ? super(new Uint32Array(n), new Uint32Array(n))
            : n.length === sparse!.length
            ? super(n, sparse!)
            : __fail();
    }

    get [Symbol.species]() {
        return SparseSet32;
    }

    get [Symbol.toStringTag]() {
        return "SparseSet32";
    }

    copy() {
        return this.__copyTo(new SparseSet32(0));
    }

    empty() {
        return new SparseSet32(this.capacity);
    }
}

/**
 * Creates a new sparse set with given max. capacity (max ID + 1) and
 * chooses most memory efficient implementation, e.g. if `n` <= 256
 * returns a {@link SparseSet8} instance.
 *
 * @param n - max capacity, ID range: [0...n)
 */
export const defSparseSet = (n: number) =>
    n <= 0x100
        ? new SparseSet8(n)
        : n <= 0x10000
        ? new SparseSet16(n)
        : new SparseSet32(n);