thi-ng/umbrella

View on GitHub
packages/associative/src/hash-map.ts

Summary

Maintainability
A
1 hr
Test Coverage
import type {
    Fn,
    Fn3,
    ICopy,
    IEmpty,
    IEquiv,
    IObjectOf,
    Maybe,
    Pair,
    Predicate2,
} from "@thi.ng/api";
import { ceilPow2 } from "@thi.ng/binary/pow";
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
import { equiv } from "@thi.ng/equiv";
import { map } from "@thi.ng/transducers/map";
import type { HashMapOpts } from "./api.js";
import { dissoc } from "./dissoc.js";
import { __equivMap } from "./internal/equiv.js";
import { __inspectable } from "./internal/inspect.js";
import { into } from "./into.js";

interface HashMapState<K, V> {
    hash: Fn<K, number>;
    equiv: Predicate2<K>;
    load: number;
    bins: Pair<K, V>[];
    mask: number;
    size: number;
}

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

/** @internal */
const __iterator = <K, V>(map: HashMap<K, V>, id: 0 | 1) =>
    function* () {
        for (let p of __private.get(map)!.bins) {
            if (p) yield p[id];
        }
    };

const DEFAULT_CAP = 16;
const DEFAULT_LOAD = 0.75;

/**
 * Configurable hash map implementation w/ ES6 Map API. Uses open
 * addressing / linear probing to resolve key collisions. Supports any
 * key types via mandatory user supplied hash function.
 *
 * See {@link HashMapOpts} for further configuration & behavior details.
 *
 * @example
 * ```ts tangle:../export/hash-map.ts
 * import { HashMap } from "@thi.ng/associative";
 * import { hash } from "@thi.ng/vectors";
 *
 * const m = new HashMap([], { hash })
 * m.set([1, 2], "a");
 * m.set([3, 4], "b");
 * m.set([1, 2], "c");
 *
 * console.log([...m]);
 * // HashMap { [ 1, 2 ] => 'c', [ 3, 4 ] => 'b' }
 * ```
 *
 */
@__inspectable
export class HashMap<K, V>
    extends Map<K, V>
    implements
        Iterable<Pair<K, V>>,
        ICopy<HashMap<K, V>>,
        IEmpty<HashMap<K, V>>,
        IEquiv
{
    constructor(pairs: Iterable<Pair<K, V>> | null, opts: HashMapOpts<K>) {
        super();
        const m = ceilPow2(Math.min(opts.cap || DEFAULT_CAP, 4)) - 1;
        __private.set(this, {
            hash: opts.hash,
            equiv: opts.equiv || equiv,
            load: opts.load || DEFAULT_LOAD,
            mask: m,
            bins: new Array(m + 1),
            size: 0,
        });
        if (pairs) {
            this.into(pairs);
        }
    }

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

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

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

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

    *entries(): IterableIterator<Pair<K, V>> {
        for (let p of __private.get(this)!.bins) {
            if (p) yield [p[0], p[1]];
        }
    }

    keys(): IterableIterator<K> {
        return __iterator(this, 0)();
    }

    values(): IterableIterator<V> {
        return __iterator(this, 1)();
    }

    /**
     * The key & value args given the callback `fn` MUST be treated as
     * readonly/immutable. This could be enforced via TS, but would
     * break ES6 Map interface contract.
     *
     * @param fn -
     * @param thisArg -
     */
    forEach(fn: Fn3<V, K, Map<K, V>, void>, thisArg?: any) {
        for (let pair of __private.get(this)!.bins) {
            fn.call(thisArg, pair[1], pair[0], this);
        }
    }

    clear() {
        const $this = __private.get(this)!;
        $this.bins = new Array(DEFAULT_CAP);
        $this.mask = 15;
        $this.size = 0;
    }

    empty() {
        return new HashMap<K, V>(null, this.opts({ cap: DEFAULT_CAP }));
    }

    copy() {
        const $this = __private.get(this)!;
        const m = new HashMap<K, V>(null, this.opts({ cap: 4 }));
        Object.assign(__private.get(m)!, {
            bins: $this.bins.slice(),
            mask: $this.mask,
            size: $this.size,
        });
        return m;
    }

    equiv(o: any) {
        return __equivMap(this, o);
    }

    has(key: K): boolean {
        const $this = __private.get(this)!;
        const i = this.find(key, $this);
        return i >= 0 && $this.bins[i] != undefined;
    }

    get(key: K, notFound?: V): Maybe<V> {
        const $this = __private.get(this)!;
        const i = this.find(key, $this);
        return i >= 0 && $this.bins[i] ? $this.bins[i][1] : notFound;
    }

    set(key: K, val: V) {
        const $this = __private.get(this)!;
        let i = this.find(key, $this);
        if (i >= 0 && $this.bins[i]) {
            $this.bins[i][1] = val;
            return this;
        }
        if ($this.size > $this.mask * $this.load) {
            this.resize($this);
            i = this.find(key, $this);
        }
        $this.bins[i] = [key, val];
        $this.size++;
        return this;
    }

    delete(key: K) {
        const $this = __private.get(this)!;
        const { bins, mask } = $this;
        let i = this.find(key, $this);
        if (i >= 0 && !bins[i]) {
            return false;
        }
        $this.size--;
        let j = i;
        let k: number;
        while (true) {
            delete bins[i];
            do {
                j = (j + 1) & mask;
                if (!bins[j]) return true;
                k = $this.hash(bins[j][0]) & mask;
            } while (i <= j ? i < k && k <= j : i < k || k <= j);
            bins[i] = bins[j];
            i = j;
        }
    }

    into(pairs: Iterable<Pair<K, V>>) {
        return <this>into(this, pairs);
    }

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

    opts(overrides?: Partial<HashMapOpts<K>>): HashMapOpts<K> {
        const $this = __private.get(this)!;
        return <HashMapOpts<K>>{
            hash: $this.hash,
            equiv: $this.equiv,
            load: $this.load,
            cap: $this.mask + 1,
            ...overrides,
        };
    }

    protected find(key: K, $this: HashMapState<K, V>) {
        const { bins, equiv, mask } = $this;
        let i = mask;
        let h = $this.hash(key) & mask;
        while (bins[h] && !equiv(bins[h][0], key)) {
            i--;
            if (i < 0) return -1;
            h = (h + 1) & mask;
        }
        return h;
    }

    protected resize($this: HashMapState<K, V>) {
        const src = $this.bins;
        const cap = ($this.mask + 1) * 2;
        $this.bins = new Array(cap);
        $this.mask = cap - 1;
        $this.size = 0;
        for (let p of src) {
            if (p) this.set(p[0], p[1]);
        }
    }
}

export function defHashMap<K, V>(
    pairs: Iterable<Pair<K, V>> | null,
    opts: HashMapOpts<K>
): HashMap<K, V>;
export function defHashMap<V>(
    obj: IObjectOf<V>,
    opts: HashMapOpts<string>
): HashMap<string, V>;
export function defHashMap<V>(
    src: any,
    opts: HashMapOpts<any>
): HashMap<any, V> {
    if (isPlainObject(src)) {
        const keys = Object.keys(src);
        return new HashMap<string, V>(
            map((k) => <Pair<string, V>>[k, (<IObjectOf<V>>src)[k]], keys),
            {
                cap: keys.length / (opts.load || DEFAULT_LOAD),
                ...opts,
            }
        );
    } else {
        return new HashMap(src, opts);
    }
}