Source: canon.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelfCanonMap = exports.canonizeByPick = exports.JsonCanonMap = exports.CanonMap = exports.jsonCanonize = exports.naiveCanonize = void 0;
const iterable_1 = require("../iterable");
const support_1 = require("../support");
/**
 * A fallible Canonizer for mapping objects to primitive versions to allow comparison by value.
 * Most primitives are mapped to themselves.
 * Strings are mapped to `"String: " + ` themselves to avoid collisions with the stringifications of other entities.
 * Arrays and objects are mapped to a stringification that digs into objects to a level defined by `maxDepth`, or 2 if `maxDepth` is not provided.
 * Dates are mapped to `"Date: "` plus their numeric value as milliseconds from epoch.
 *
 * @param {K} lookup The key to canonize.
 * @param {number} maxDepth? The maximum number of levels to descend into nested objects and arrays when stringifying.
 * @returns A canonized version of the lookup. Not necessarily a string but guaranteed to be a primitive.
 */
function naiveCanonize(lookup, maxDepth = 2) {
    if (typeof lookup === 'object' && lookup !== null) {
        if (maxDepth === 0) {
            return String(lookup);
        }
        else {
            if (Array.isArray(lookup)) {
                return "[" + lookup.map(l => naiveCanonize(l, maxDepth - 1)).join(", ") + "]";
            }
            else if (lookup instanceof Date) {
                return "Date: " + lookup.valueOf();
            }
            else {
                // Non-recursive stringify
                return "{"
                    + iterable_1.collect(iterable_1.entries(lookup)).sort((entry1, entry2) => entry1[0] > entry2[0] ? 1 : -1).map(([key, val]) => key + ": " + naiveCanonize(val, maxDepth - 1)).join(", ")
                    + "}";
            }
        }
    }
    else if (typeof lookup === 'string') {
        return "String: " + lookup;
    }
    else if (Number.isNaN(lookup)) {
        return "Number: NaN";
    }
    else {
        return lookup;
    }
}
exports.naiveCanonize = naiveCanonize;
/**
 * Canonize using `JSON.stringify()`.
 *
 * @remarks
 * This canonization algorithm works better than the naïve one for nested objects, but it is subject to all the gotchas of JSON, e.g.:
 * - `undefined` mapped to `null` when it appears in arrays, not mapped at all when it appears in an object
 * - `NaN` mapped to `null`
 * - Fails on circular references
 *
 * @param  {K} lookup The key to canonize.
 * @returns A stringified version of the lookup.
 */
function jsonCanonize(lookup) {
    return JSON.stringify(lookup);
}
exports.jsonCanonize = jsonCanonize;
/**
 * Map with canonized keys.
 * By instantiating a Map with a canonizer function, we can execute lookups according to an arbitrary notion of key identity.
 * For example, we can use arrays as Map indices and treat arrays with the same members as equivalent.
 *
 * @remarks
 * In use cases that call for Maps, developers will often want to map by some combination of values instead of a single value.
 * If they use an object or array for this, however, lookups won't work because objects or arrays are compared by reference rather than value.
 * The solution is to initialize the map with a "canonizer" that, for any key, creates a canonical equivalent that other referentially unique objects and arrays can map to.
 *
 * This gives the user total control over the equality algorithm. Under the hood, CanonMap maintains a Map between canonized keys and values. As far as TypeScript's type system is concerned, though, it is a Map from the desired key type to the desired value type that just happens to compare by equality.
 *
 * @extends Map
 */
class CanonMap extends Map {
    /**
     * Initialize a CanonMap.
     *
     * @typeparam K Key type.
     * @typeparam T Value type.
     * @param entries? {Iterable}
     * An iterable yielding all key-value tuples that will be fed into the Map.
     * Without this, the Map is initialized to empty.
     * @param {Canonizer | number} canonizer? Function to map keys to suitable primitives.
     * If not provided, the CanonMap will use a default canonizer.
     * If a number is provided, that number will be the recursion depth of the default canonizer, overriding the default depth of 2.
     */
    constructor(entries, canonizer = naiveCanonize) {
        super();
        if (typeof canonizer === "number") {
            this.canonizer = (k) => naiveCanonize(k, canonizer);
        }
        else {
            this.canonizer = canonizer;
        }
        if (entries) {
            for (let entry of entries) {
                const [key, value] = entry;
                // @ts-ignore
                super.set(canonizer(key), [key, value]);
            }
        }
    }
    /**
     * Get the value at the canonized key in the CanonMap object.
     * Returns the CanonMap object.
     *
     * @param {K} key The key to look up.
     * @returns {T | undefined} The value if found, `undefined` otherwise.
     */
    get(key) {
        const canon = this.canonizer(key);
        return super.has(canon) ? super.get(canon)[1] : undefined;
    }
    /**
     * Get the value at the canonized key in the CanonMap object.
     * Returns the CanonMap object.
     *
     * @param {K} key The key to look up.
     * @returns {boolean} `true` if the canonization of the key is present in the CanonMap, `false` otherwise.
     */
    has(key) {
        return super.has(this.canonizer(key));
    }
    /**
     * Set the value for the canonized key in the CanonMap object.
     * Returns the CanonMap object.
     *
     * @param {K} key The key to set.
     * @param {T} val The value to set at that key.
     */
    set(key, val) {
        // @ts-ignore
        super.set(this.canonizer(key), [key, val]);
        return this;
    }
    /**
     * Delete the key-value pair associated with the canonized `key`.
     * Does nothing if that entry is not present.
     *
     * @param {K} key The key to delete the canonization of.
     * @returns `true` if an element in the CanonMap object existed and has been removed, `false` if the element does not exist.
     */
    delete(key) {
        return super.delete(this.canonizer(key));
    }
    *[Symbol.iterator]() {
        for (let entry of super[Symbol.iterator]()) {
            yield entry[1];
        }
    }
    *entries() {
        for (let entry of this[Symbol.iterator]()) {
            yield entry;
        }
    }
    *keys() {
        for (let entry of this[Symbol.iterator]()) {
            yield entry[0];
        }
    }
    *values() {
        for (let entry of this[Symbol.iterator]()) {
            yield entry[1];
        }
    }
    forEach(callbackfn, thisArg) {
        for (let entry of this[Symbol.iterator]()) {
            callbackfn.call(thisArg, entry[1], entry[0]);
        }
    }
}
exports.CanonMap = CanonMap;
/**
 * Create a CanonMap that canonizes using `JSON.stringify`.
 *
 * @param entries? The entries with which to initialize the map.
 * By default, creates an empty map.
 */
function JsonCanonMap(entries) {
    return new CanonMap(entries, jsonCanonize);
}
exports.JsonCanonMap = JsonCanonMap;
function* mapIterable(iter, mapper) {
    for (const i of iter) {
        yield mapper(i);
    }
}
/**
 *
 * @param _ Example of what will be picked. If necessary, forcefully cast this to the type desired.
 * @param pick An array of the key lookups to perform.
 * @returns Canonizer that produces a canonical string by combining all specified key-value pairs together.
 * @warning This will not maintain distinctness for non-primitive objects.
 */
function canonizeByPick(_, pick) {
    return (o) => pick.map(k => `${String(k)}:${support_1.defined(o[k])}`).join('|');
}
exports.canonizeByPick = canonizeByPick;
/**
 * A version of CanonMap that is specialized for creating indexes: mapping an object's identifying attributes, specified by canonization, to the object itself.
 *
 * @example
 * Take this data type Duck: { name: string, featherCount: number ).
 * Ducks are identified by their name, so we can use a SelfCanonMap both to store the canonical version of a Duck, and to look up a Duck knowing its name only:
 *
 * const duckMap = new SelfCanonMap([{ name: 'Rodney', featherCount: 13217 }, { name: 'Ellis', featherCount: 11992 }], ({name}) => name)
 * duckMap.get({name: 'Ellis'}) // { name: 'Ellis', featherCount: 11992 }
 */
class SelfCanonMap extends CanonMap {
    constructor(pick, entries) {
        const unwrappedEntries = mapIterable(entries || [], i => [i, i]);
        const canonizer = canonizeByPick(null, pick);
        super(unwrappedEntries, canonizer);
    }
    /**
     * Adds the value to the SelfCanonMap.
     * If any value collides with its canonization, that value will be overwritten.
     *
     * @param {T} val The value to set.
     * @returns the SelfCanonMap object.
     */
    add(val) {
        // @ts-ignore
        this.set(val, val);
        return this;
    }
    /**
     * Adds values to the SelfCanonMap.
     * If any value collides with its canonization, that value will be overwritten.
     *
     * @param {T} vals The values to add.
     * @returns the SelfCanonMap object.
     */
    fill(vals) {
        for (const val of vals) {
            // @ts-ignore
            this.add(val);
        }
        return this;
    }
}
exports.SelfCanonMap = SelfCanonMap;