"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolutionFailureMessage = exports.mapCollectIntoBumping = exports.mapCollectBumping = exports.zipMapsUnion = exports.zipMapsIntersection = exports.consume = exports.getOption = exports.mapToDictionary = exports.rekeyBinMap = exports.invertBinMap = exports.reconcileAddToSet = exports.reconcileFirst = exports.reconcileDefault = exports.reconcileInit = exports.reconcileFold = exports.reconcileConcat = exports.reconcileCount = exports.reconcileAdd = exports.reconcileAppend = exports.flatMakeEntries = exports.binMap = exports.keyBy = exports.makeEntries = exports.getOrFill = exports.getOrFail = exports.getOrElse = exports.reconcileEntryInto = exports.foldingGet = exports.getOrVal = exports.selectMap = exports.uniformMap = exports.valuesOf = exports.keysOf = exports.mapKeys = exports.mapValues = exports.invertMap = exports.partitionCollect = exports.mapCollect = exports.concatMap = exports.mapCollectInto = void 0;
const iterable_1 = require("../iterable");
const support_1 = require("../support");
/**
* Insert the entries in the iterable into the provided map.
* If two values map to the same key and the `reconcileFn` argument is provided, it will be called to combine the colliding values to set the final value; otherwise, the last value to arrive at that key will overwrite the rest.
*
* @param {Iterable} iterable The entries to add.
* @param {Map} seed The Map to add them to.
* @param {Reconciler} reconcileFn?
* A function specifying what value to set when two keys map to the same value.
* If provided, this is called whether there is a collision or not, so it also serves as a mapper.
* Called with:
* 1. The value previously set at this key, or `undefined` if no value was set;
* 2. The new value arriving from the Iterable;
* 3. The key where the output will be entered.
* @returns The updated Map.
*/
function mapCollectInto(iterable, seed, reconcileFn) {
if (reconcileFn) {
for (let [key, val] of iterable) {
const got = seed.get(key);
const reconciled = reconcileFn(got, val, key);
if (reconciled === undefined) {
seed.delete(key);
}
else if (reconciled !== got) {
seed.set(key, reconciled);
}
}
}
else {
for (let entry of iterable) {
const [key, val] = entry;
seed.set(key, val);
}
}
return seed;
}
exports.mapCollectInto = mapCollectInto;
/**
* Combine Iterables of Map entries into a single Iterable, leaving keys unmerged.
*
* @param maps The Map Iterables to merge
* @returns An Iterable consisting of *all* entries of the Iterables in the arguments, even those with duplicate keys.
*/
function concatMap(...maps) {
return iterable_1.combine(...maps);
}
exports.concatMap = concatMap;
/**
* Convert an Iterable of Map entries into a brand new map.
* When called on a map, the result will be a new Map with the same entries as the previous one.
* If two values map to the same key and the `reconcileFn` argument is provided, it will be called to combine the colliding values to set the final value; otherwise, the last value to arrive at that key will overwrite the rest.
*
* @param {Iterable} iterable The entries to add.
* @param {Reconciler} reconcileFn?
* A function specifying what value to set when two keys map to the same value.
* If provided, this is called whether there is a collision or not, so it also serves as a mapper.
* Called with:
* 1. The value previously set at this key, or `undefined` if no value was set;
* 2. The new value arriving from the Iterable;
* 3. The key where the output will be entered.
* @returns The newly created Map.
*/
function mapCollect(iterable, reconcileFn) {
return mapCollectInto(iterable, new Map(), reconcileFn);
}
exports.mapCollect = mapCollect;
/**
* Generate keys for each item in an Iterable. Sort the items into bins based on the key they generated.
*
* If `guaranteeKeys` is supplied, bins with these keys are guaranteed to exist in the result even if no items generated that key.
*
* @remarks
* This composes some common steps of mapCollect together.
*
* @param iterable The input items.
* @param keyFn A function to generate keys.
* @param guaranteeKeys? A list of keys that must exist in the output.
*/
function partitionCollect(iterable, keyFn, guaranteeKeys) {
const seed = guaranteeKeys ? new Map(guaranteeKeys.map(key => [key, []])) : new Map();
return mapCollectInto(keyBy(iterable, keyFn), seed, reconcileAppend());
}
exports.partitionCollect = partitionCollect;
/**
* Reverse a stream of entries so that entries of the form [key, value] are now in the form [value, key].
*
* Any key collisions must be handled in later steps, or they will be reconciled automatically by later entries overriding earlier ones.
*
* @param {Iterable} iterable An iterable representing the entries of a Map from key to value.
* @returns An iterable representing the entries of a Map from value to key.
*/
function invertMap(iterable) {
return iterable_1.map(iterable, ([k, t]) => [t, k]);
}
exports.invertMap = invertMap;
/**
* Given a Map-like Iterable, produce an entry set for a new Map where each key has been mapped to a new key by calling ${mapper}.
*
* @param {Iterable} iterable An iterable representing the entries of a Map from key to value.
* @param {Function} mapper A function mapping the values of the Map to a transformed value.
* @returns An iterable representing the entries of a map from key to the transformed value.
*/
function mapValues(iterable, mapper) {
return iterable_1.map(iterable, ([key, val]) => [key, mapper(val, key)]);
}
exports.mapValues = mapValues;
/**
* Given a Map-like Iterable, produce an entry set for a new Map where each key has been mapped to a new key by calling ${keyMapper}.
*
* Any key collisions must be handled in later steps, or they will be reconciled automatically by later entries overriding earlier ones.
*
* @param {Iterable} iterable An iterable representing the entries of a Map from key to value.
* @param {Function} fn A function mapping the keys of the Map to a transformed key.
* @returns An iterable representing the entries of a map from the transformed key to value.
*/
function mapKeys(iterable, keyMapper) {
return iterable_1.map(iterable, ([key, val]) => [keyMapper(key, val), val]);
}
exports.mapKeys = mapKeys;
/**
* Get an Iterable containing the keys of a Map or Map-like Iterable.
*
* @param {Iterable} iterable An iterable representing the entries of a Map from key to value.
* @returns An iterable representing the keys of the map.
*/
function keysOf(iterable) {
return iterable_1.map(iterable, arr => arr[0]);
}
exports.keysOf = keysOf;
/**
* Get an Iterable containing the values of a Map or Map-like Iterable.
*
* @param {Iterable} iterable An iterable representing the entries of a Map from key to value.
* @returns An iterable representing the values of the map.
*/
function valuesOf(iterable) {
return iterable_1.map(iterable, arr => arr[1]);
}
exports.valuesOf = valuesOf;
/**
* Create a Map-like Iterable from an Iterable of keys where each key maps to the same value.
*
* @param {Iterable} iterable An iterable representing the keys of a Map.
* @param {T} of The fixed value to set all keys to.
* @returns An Iterable representing the entries of a Map from the keys each to the same fixed value.
*/
function uniformMap(keys, of) {
return iterable_1.map(keys, key => [key, of]);
}
exports.uniformMap = uniformMap;
/**
* Filter out key-value pairs from a Map or Map-like Iterable.
*
* @param {Iterable} iterable An iterable representing the entries of a Map.
* @param {Function} filterFn A function that returns true if the entry is to be included, false otherwise.
* @returns An iterable representing the entries of a Map without all those entries for which `filterFn` returned `false`.
*/
function selectMap(iterable, filterFn) {
return iterable_1.filter(iterable, ([key, val]) => filterFn(val, key));
}
exports.selectMap = selectMap;
/**
* Retrieve a value from the Map at the given key. If it is not present, return ${substitute} instead.
*
* @param {Map} map The map on which to perform the lookup.
* @param {T} key The key to look up.
* @param {V} substitute The value to return if the key is not present.
* @returns The value at the key in the map, or the substitute if that key is not present.
*/
function getOrVal(map, key, substitute) {
if (map.has(key)) {
return map.get(key);
}
else {
return substitute;
}
}
exports.getOrVal = getOrVal;
/**
* Retrieve a value from the Map at the given key. If the value was retrieved, map it with ${ifPresent}; if not, return the result or calling ${ifAbsent}.
*
* @param {Map} map The map on which to perform the lookup.
* @param {T} key The key to look up.
* @param {Function} ifPresent The function to call on the value and `key` if the value is present.
* @param {Function} ifAbsent? The function to call with `key` if the value is absent, by default a noop returning `undefined`.
* @returns the result of calling `ifPresent` on a value if that value is at `key` in `map`, the result of calling `ifAbsent` otherwise.
*/
function foldingGet(map, key, ifPresent, ifAbsent = (() => { })) {
if (map.has(key)) {
return ifPresent(map.get(key), key);
}
else {
return ifAbsent(key);
}
}
exports.foldingGet = foldingGet;
/**
* Set a value on Map, using a Reconciler to merge the incoming value with any existing value.
*
* This simulates the behaviour of merging a value in MapCollect, but for a single value instead of an Iterable.
*
* WARNING: This includes the behaviour that, if the Reconciler returns `undefined`, the entry at the Map will be deleted.
*
* @param map The Map to set the key-value pair on.
* @param key The key to set.
* @param value The value to reconcile with any possible colliding value in Map.
* @param reconciler The reconciler function.
* @returns The value ultimately set.
*/
function reconcileEntryInto(map, key, value, reconciler) {
const reconciled = reconciler(map.get(key), value, key);
if (reconciled !== undefined) {
map.set(key, reconciled);
}
else {
map.delete(key);
}
return reconciled;
}
exports.reconcileEntryInto = reconcileEntryInto;
/**
* Retrieve a value from the Map at the given key. If the key is not set, return an alternate value by calling ${substitute}.
*
* @param {Map} map The map on which to perform the lookup.
* @param {T} key The key to look up.
* @param {Function} substitute The function to call on `key` if the value is not present.
* @returns the value at `key` in `map` if that value exists, the result of calling `substitute` otherwise.
*/
function getOrElse(map, key, substitute) {
if (map.has(key)) {
return map.get(key);
}
else {
return substitute(key);
}
}
exports.getOrElse = getOrElse;
/**
* Retrieve a value from the Map at the given key, throwing an error if the key was not set.
*
* @param {Map} map The map on which to perform the lookup.
* @param {T} key The key to look up.
* @param {string | Function} error? The error to generate if the key is not present. Can be a function taking the key as a parameter, or an explicit string.
* @returns the value at `key` in `map` if that value exists, the result of calling `substitute` otherwise.
* @throws The specified error if an error string or function is provided, a default error message if not.
*/
function getOrFail(map, key, error) {
return getOrElse(map, key, (key) => {
throw new Error(typeof error === "function"
? error(key)
: typeof error === "undefined"
? `Map has no entry "${key}"`
: error);
});
}
exports.getOrFail = getOrFail;
/**
*
* Retrieve a value from the Map at the given key. If the value does not exist, initialize
* a value at the specified key with `freshFn`, then return that value.
*
* @param map The map on which to perform the lookup.
* @param key The key to look up.
* @param freshFn The function to call with the key to generate a fresh value.
*/
function getOrFill(map, key, freshFn) {
return foldingGet(map, key, (i) => i, (key) => {
const fresh = freshFn(key);
map.set(key, fresh);
return fresh;
});
}
exports.getOrFill = getOrFill;
/**
* Convert an iterable of values into a list of Map entries with a mapping function.
*
* @param {Iterable} arr The input iterable.
* @param {Function} mapFn The function that turns inputs into entries.
* @returns An iterable of key-value tuples generated by the output of `mapFn` when called on members of `arr`.
*/
function makeEntries(arr, mapFn) {
return iterable_1.map(arr, mapFn);
}
exports.makeEntries = makeEntries;
/**
* Produce an Iterable from a list of values and a function to produce a key for each value.
*
* Does not check collisions; these can be handled at a later step.
*
* @param arr The values to map.
* @param keyFn The function to generate keys.
* @returns An Iterable representing key-value pairs where the keys are generated by calling `keyFn` on the values.
*/
function* keyBy(arr, keyFn) {
for (let val of arr) {
yield [
keyFn(val),
val
];
}
}
exports.keyBy = keyBy;
/**
* Generate a Map that stores the keys of incoming values, as produced by ${keyFn}, and maps them to the full array of values that produced those keys.
*
* For those familiar with SQL, this is functionally a "GROUP BY" query that groups values by combining them into arrays.
*
* @param {Iterable} arr The Iterable to map over.
* @param keyFn The function to generate keys with.
*/
function binMap(arr, keyFn, seed) {
const ret = seed || new Map();
iterable_1.forEach(arr, val => {
const key = keyFn(val);
const current = getOrVal(ret, key, []);
current.push(val);
ret.set(key, current);
});
return ret;
}
exports.binMap = binMap;
/**
* Convert an iterable of values into a sequence of Map entries, pairing each value with a series of keys as returned by ${expandFn}.
*
* Where @{expandFn} returns no keys, the value will be ignored; where it returns multiple keys, an entry will be created for each key.
*
* @param {Iterable} arr The input iterable.
* @param {Function} expandFn The function that turns the input into a (possibly empty) list of entries.
* @returns An iterable of key-value tuples generated by appending together the output of `expandFn` when called on members of `arr`.
*/
function* flatMakeEntries(arr, expandFn) {
for (let val of arr) {
yield* expandFn(val);
}
}
exports.flatMakeEntries = flatMakeEntries;
/**
* Generate a Reconciler that pushes input values onto an array of previously colliding values, optionally transforming them first with a mapper.
*
* @param {Function} mapFn? A function to call on the inputs.
* @returns {Reconciler} A Reconciler that combines input values into an Array.
*/
function reconcileAppend(mapFn) {
if (mapFn) {
return function (collidingValue, value) {
const val = mapFn(value);
if (collidingValue === undefined) {
return [val];
}
else {
collidingValue.push(val);
return collidingValue;
}
};
}
else {
return function (collidingValue, value) {
if (collidingValue === undefined) {
return [value];
}
else {
collidingValue.push(value);
return collidingValue;
}
};
}
}
exports.reconcileAppend = reconcileAppend;
/**
* Generate a Reconciler that either adds a numeric input value to a colliding numeric value, or maps the input value to a number before doing so.
*
* @param {Function} mapFn A function that maps incoming values to numbers so they can be reconciled by adding.
* @returns {Reconciler} A summing Reconciler.
*/
function reconcileAdd(mapFn) {
return function (collidingValue, value) {
const val = mapFn ? mapFn(value) : value;
if (collidingValue === undefined) {
return val;
}
else {
return val + collidingValue;
}
};
}
exports.reconcileAdd = reconcileAdd;
/**
* Generate a Reconciler that bumps up a count on each collision, ultimately yielding the total number of entries that collided on a key.
*
* @returns {Reconciler} A Reconciler that counts entries that has the same key.
*/
function reconcileCount() {
return function (collidingValue, _) {
if (collidingValue === undefined) {
return 1;
}
else {
return 1 + collidingValue;
}
};
}
exports.reconcileCount = reconcileCount;
/**
* Generate a Reconciler that concatenates input values together when they collide, optionally transforming them first with a mapper.
*
* @param {Function} mapFn? A function to call on the inputs.
* Regardless of the input type, the output must be an Iterable so it can be concatenated into the Map.
* @returns {Reconciler} A Reconciler that concatenates input values together.
*/
function reconcileConcat(mapFn = (val) => val) {
return function (collidingValue, value) {
const val = mapFn(value);
if (collidingValue === undefined) {
return Array.from(val);
}
else {
return [...collidingValue, ...val];
}
};
}
exports.reconcileConcat = reconcileConcat;
/**
* Generate a Reconciler by specifying a function to run by default, and a second function to run if a value already exists in the Map at the specified key.
*
* @remarks
* This is an alternate dialect for generating a Reconciler that saves the boilerplate of `if () {} else {}` at the cost of having to define two different functions.
*
* @param mapper A function that takes an incoming value and returns the value to set.
* @param reducer A function that takes the colliding value and an incoming value and returns the value to set.
*
* @returns A Reconciler that calls `mapper` if a collidingValue exists (even if it is `undefined`!), calls `reducer` otherwise.
*/
function reconcileFold(mapper, reducer) {
return function (collidingValue, value) {
if (collidingValue === undefined) {
return mapper(value);
}
else {
return reducer(collidingValue, value);
}
};
}
exports.reconcileFold = reconcileFold;
/**
* Generate a Reconciler by specifying a function to generate the initial value if none exists, and a second function to run to merge the incoming value with either the preexisting value or the initial value depending on the case.
*
* @remarks
* This is an alternate dialect for generating a Reconciler that saves the boilerplate of `const toMerge = colliding === undefined ?initial() : colliding;` at the cost of having to define two different functions.
* It makes the function behave like a traditional reducer, with a zip function and an initializer.
*
* @param reducer A function that merges a colliding value and an incoming value.
* @param initial A function that generates the first colliding value for when a colliding value does not exist.
*
* @returns A Reconciler that calls `mapper` if a collidingValue exists (even if it is `undefined`!), calls `reducer` otherwise.
*/
function reconcileInit(initializer, reducer) {
return function (collidingValue, value) {
if (collidingValue === undefined) {
return reducer(initializer(value), value);
}
else {
return reducer(collidingValue, value);
}
};
}
exports.reconcileInit = reconcileInit;
/**
* Generate a reconciler that simulates the default behaviour of setting Maps, overwriting any value that was already at the key on `set`.
* @returns {Reconciler} A Reconciler that always returns the `incomingValue`.
*/
function reconcileDefault() {
return function (_, value) {
return value;
};
}
exports.reconcileDefault = reconcileDefault;
/**
* Generate a reconciler that reverses the default behaviour of setting Maps: instead of overwriting what's already at a key, the `set` operation is ignored if a value is already present at that key.
* @returns {Reconciler} A Reconciler that returns the `collidingValue` if it is defined, the `incomingValue` otherwise.
*/
function reconcileFirst() {
return function (collidingValue, incomingValue) {
if (collidingValue === undefined) {
return incomingValue;
}
else {
return collidingValue;
}
};
}
exports.reconcileFirst = reconcileFirst;
/**
* Generate a reconciler for collecting Sets on a map.
*
* @returns {Reconciler} A Reconciler that adds the value to a Set or initializes a Set with that member if not.
*/
exports.reconcileAddToSet = () => (colliding, incoming) => {
if (colliding === undefined) {
return new Set([incoming]);
}
else {
colliding.add(incoming);
return colliding;
}
};
/**
* Convert a map from keys to arrays of values (i.e., of the form Map<K, T[]>) to a map of values from arrays of keys (i.e., of the form Map<T, K[]>).
*
* @example
* const peopleToFlavours = new Map([
* ["Alex", ["vanilla"]],
* ["Desdemona", ["banana", "chocolate"],
* ["Henrietta", ["vanilla", "chocolate", "cherry"]
* ]);
*
* const flavoursToPeople = new Map([
* ["vanilla", ["Alex", "Henrietta"]],
* ["banana", ["Desdemona"]],
* ["chocolate", ["Desdemona", "Henrietta"]],
* ["cherry", ["Henrietta"]]
* ]);
*
* assert(deepEquals(
* Array.from(flavoursToPeople),
* Array.from(invertBinMap(peopleToFlavours))
* ));
*
* @param {Iterable} map An Iterable representing a Map of entries where the values are arrays.
* @returns {Map} A Map containing, for each member value that appears in any of the arrays, an entry where the key is the value in the array and the value is a list of all the keys in the input Map that included it.
*/
function invertBinMap(map) {
return mapCollect(iterable_1.flatMap(map, ([key, arr]) => arr.map(t => support_1.tuple([t, key]))), reconcileAppend());
}
exports.invertBinMap = invertBinMap;
/**
* Convert a map from keys to arrays of values (i.e., of the form Map<K, T[]>) to a map of different keys to arrays of values (i.e. of the form Map<K2, T[]>) with a re-keying function that takes the value and its current key.
*
* This is most useful when a collection of objects has been grouped by one of its properties and, after operating on it, you need to group it by a different one of its properties.
*
* @example
const peopleToFlavours = new Map([
["Alex", [{name: "Alex", flavour: "vanilla"}]],
["Desdemona", [{name: "Desdemona", flavour: "banana"}, {name: "Desdemona", flavour: "chocolate"}]],
["Alexa", [{name: "Alexa", flavour: "vanilla"}, {name: "Alexa", flavour: "chocolate"}, {name: "Alexa", flavour: "cherry"}]]
]);
const flavoursToPeople = new Map([
["vanilla", [{name: "Alex", flavour: "vanilla"}, {name: "Alexa", flavour: "vanilla"}]],
["banana", [{name: "Desdemona", flavour: "banana"}]],
["chocolate", [{name: "Desdemona", flavour: "chocolate"}, {name: "Alexa", flavour: "chocolate"}]],
["cherry", [{name: "Alexa", flavour: "cherry"}]]
]);
should.deepEqual(
Array.from(flavoursToPeople),
Array.from(rekeyBinMap(peopleToFlavours, val => val.flavour))
);
*
* @param {Iterable} map An Iterable representing a Map of entries where the values are arrays.
* @param {Function} keyBy The function used to generate a new key for each member element of each bin.
* First argument: the value in the bin
* Second argument: the key of the bin
* @returns {Map} A Map containing, for each member value that appears in any of the arrays, an entry where the key is the value in the array and the value is a list of all the keys in the input Map that included it.
*/
function rekeyBinMap(map, keyBy) {
return mapCollect(iterable_1.flatMap(map, ([key, arr]) => arr.map(t => support_1.tuple([keyBy(t, key), t]))), reconcileAppend());
}
exports.rekeyBinMap = rekeyBinMap;
/**
* Convert a Map into a dictionary.
*
* @remarks This is handy when the contents of map need to be serialized to JSON.
*
* @param {Iterable} map An iterable of Map entries.
* @param {Function} stringifier? A function to convert a Map key into a string key that is suitable for use in a dictionary. If excluded, `mapToDictionary` will use the default String constructor.
* @returns The new dictionary object.
*/
function mapToDictionary(map, stringifier = String) {
const ret = {};
for (let entry of map) {
const [key, val] = entry;
ret[stringifier(key)] = val;
}
return ret;
}
exports.mapToDictionary = mapToDictionary;
/**
* Get an fp-ts Option representing the result of a map lookup.
*
* @param map The map to search on
* @param key The key to look up
* @returns Some(value) if a value is present on the map at the key, None if not.
*/
function getOption(map, key) {
if (map.has(key)) {
return support_1.some(map.get(key));
}
else {
return support_1.none;
}
}
exports.getOption = getOption;
/**
* Try to grab a value from a map, returning it wrapped in Some if present, and removing it from the map.
*
* @param map The map to look up on
* @param key The key to look up
* @returns An fp-ts Option representing success on the lookup.
* @effect The entry at the key is deleted.
*/
function consume(map, key) {
return foldingGet(map, key, (val) => {
map.delete(key);
return support_1.some(val);
}, () => support_1.none);
}
exports.consume = consume;
/**
* Combine two Maps into a stream of entries of the form `[commonKeyType, [valueInFirstMap, valueInSecondMap]]`.
* If a key is in one Map but not the other, that key will not be represented in the output.
* To include them, use {@link zipMapsUnion}.
*
* @remarks
* Internally, `zipMapsIntersection` turns `map2` into a Map if it isn't one already.
* This is because it needs random access into `map2` while looping over `map1` to get the values to zip.
*
* @param map1
* @param map2
* @returns {Iterable} An iterable of the form `Map<commonKeyType, [valueInFirstMap, valueInSecondMap]>` containing all keys that `map1` and `map2` have in common.
*/
function* zipMapsIntersection(map1, map2) {
const map2Collect = map2 instanceof Map ? map2 : mapCollect(map2);
for (let [key, value1] of map1) {
if (map2Collect.has(key)) {
yield [key, [value1, getOrFail(map2Collect, key)]];
}
}
}
exports.zipMapsIntersection = zipMapsIntersection;
/**
* Combine two Maps into a stream of entries of the form `[commonKeyType, [valueInFirstMap | undefined], [valueInSecondMap | undefined]]`.
* If a key is in one Map but not the other, the output tuple will contain `undefined` in place of the missing value.
* To exclude them instead, use {@link zipMapsIntersection}.
*
* @remarks
* Internally, `zipMapsUnion` must collect all non-Map Iterables into Maps so it can look up what keys exist in `map2` during the initial pass over `map1`, and then to determine which keys have already been yielded during the second pass over `map2`.
* This probably makes it slower than {@link zipMapsIntersection} in this case.
*
* @param map1
* @param map2
* @returns {Iterable} An iterable of the form `Map<commonKeyType, [valueInFirstMap | undefined, valueInSecondMap | undefined]>` containing all keys that exist in either `map1` or `map2`.
*/
function* zipMapsUnion(map1, map2) {
const map1Collect = map1 instanceof Map ? map1 : mapCollect(map1);
const map2Collect = map2 instanceof Map ? map2 : mapCollect(map2);
for (let [key, value1] of map1Collect) {
yield [key, [value1, map2Collect.get(key)]];
}
for (let [key, value2] of map2Collect) {
if (!map1Collect.has(key)) {
yield [key, [undefined, value2]];
}
}
}
exports.zipMapsUnion = zipMapsUnion;
/**
* Pipe the entries of a Map iterable into a Map, resolving key collisions by setting the incoming entry to a new key determined by `bumper`.
* If the new key collides too, keep calling `bumper` until it either resolves to a unique key or returns `undefined` to signal failure.
*
* @param {Iterable} mapEnumeration An entry stream with duplicate keys.
* @param {BumperFn} bumper A function to be called each time a key would overwrite a key that has already been set in `seed`.
* @param {Map} seed The Map to insert values into.
* @returns {{Map}} The finalized Map.
*/
function mapCollectBumping(mapEnumeration, bumper) {
return mapCollectIntoBumping(mapEnumeration, bumper, new Map());
}
exports.mapCollectBumping = mapCollectBumping;
/**
* Pipe the entries of a Map iterable into a Map, resolving key collisions by setting the incoming entry to a new key determined by `bumper`.
* If the new key collides too, keeps calling `bumper` until it either resolves to a unique key or returns `undefined` to signal failure.
*
* @remarks
* The `priorBumps` parameter can be used to fail key generation if too many collisions occur, either by returning `undefined` or by throwing an appropriate error (see {@link resolutionFailureMessage}).
* For complex functions, this is the only guaranteed way to avoid entering an infinite loop.
*
* @param {Iterable} mapEnumeration An entry stream with duplicate keys.
* @param {BumperFn} bumper A function to be called each time a key would overwrite a key that has already been set in `seed`.
* @param {Map} seed The Map to insert values into.
* @returns {{Map}} The finalized Map.
*/
function mapCollectIntoBumping(mapEnumeration, bumper, seed) {
for (let [key, value] of mapEnumeration) {
if (seed.has(key)) {
let newKey = key;
let attempts = 0;
while (true) {
const innerNewKey = bumper(newKey, attempts++, key, getOrFail(seed, key), value);
if (innerNewKey === undefined) {
// Failed to set
break;
}
else if (!seed.has(innerNewKey)) {
seed.set(innerNewKey, value);
break;
}
else {
newKey = innerNewKey;
}
}
}
else {
seed.set(key, value);
}
}
return seed;
}
exports.mapCollectIntoBumping = mapCollectIntoBumping;
/**
* Function that a caller of `bumpDuplicateKeys()` can use to produce a generic error message when a key collision cannot be resolved.
*
* @param collidingKey The key that could not be resolved.
* @param priorBumps The number of attempts made before the bumper gave up.
* @returns {string} A message describing the error
*/
function resolutionFailureMessage(collidingKey, priorBumps) {
const pluralize = (n) => n === 1 ? "try" : "tries";
return `Failed to resolve key "${collidingKey}" to a unique value after ${priorBumps} ${pluralize(priorBumps)}`;
}
exports.resolutionFailureMessage = resolutionFailureMessage;