ixfx
Version:
Bundle of ixfx libraries
1,631 lines (1,614 loc) • 112 kB
JavaScript
import { n as __exportAll } from "./chunk-CaR5F9JI.js";
import { C as numberTest, M as resultThrow, o as testPlainObjectOrPrimitive, u as nullUndefTest, v as integerTest } from "./src-C_hvyftg.js";
import { n as getErrorMessage } from "./src-B7f_ks6F.js";
//#region ../packages/core/src/records/circular.ts
const removeCircularReferences = (value, replaceWith = null, seen = /* @__PURE__ */ new WeakSet(), path = ``) => {
if (value === null) return value;
if (typeof value !== `object`) throw new TypeError(`Param 'value' must be an object. Got type: ${typeof value}`);
seen.add(value);
const entries = Object.entries(value);
for (const entry of entries) {
if (entry[1] === null) continue;
if (typeof entry[1] !== `object`) continue;
if (seen.has(entry[1])) {
entry[1] = replaceWith;
continue;
}
entry[1] = removeCircularReferences(entry[1], replaceWith, seen, `${entry[0]}.`);
}
return Object.fromEntries(entries);
};
//#endregion
//#region ../packages/core/src/records/clone-from-fields.ts
const cloneFromFields = (source) => {
const entries = [];
for (const field in source) {
const value = source[field];
if (testPlainObjectOrPrimitive(value)) entries.push([field, value]);
}
return Object.fromEntries(entries);
};
//#endregion
//#region ../packages/core/src/records/map-object-keys.ts
/**
* Maps the keys of an object, returning a transformed object.
* ```js
* const input = {
* hello: `there`,
* chap: `chappie`
* }
*
* mapObjectKeys(input, key => key.toUppercase());
*
* // Yields: { HELLO: `there`, CHAP: `chappie` }
* ```
* @param object
* @param mapFunction
* @returns
*/
const mapObjectKeys = (object, mapFunction) => {
const destinationObject = {};
for (const entries of Object.entries(object)) {
const key = mapFunction(entries[0]);
destinationObject[key] = entries[1];
}
return destinationObject;
};
//#endregion
//#region ../packages/core/src/records/compare.ts
/**
* Compares the keys of two objects, returning a set of those in
* common, and those in either A or B exclusively.
* ```js
* const a = { colour: `red`, intensity: 5 };
* const b = { colour: `pink`, size: 10 };
* const c = compareObjectKeys(a, b);
* // c.shared = [ `colour` ]
* // c.a = [ `intensity` ]
* // c.b = [ `size` ]
* ```
* @param a
* @param b
* @returns
*/
const compareObjectKeys = (a, b) => {
return compareIterableValuesShallow(Object.keys(a), Object.keys(b));
};
/**
* Returns the changed fields from A -> B. It's assumed that A and B have the same shape.
* ie. returns an object that only consists of fields which have changed in B compared to A.
*
* ```js
* const a = { msg: `hi`, v: 10 };
*
* changedProperties(a, { msg: `hi`, v: 10 }); // {}
* changedProperties(a, { msg: `hi!!`, v: 10 }); // { msg: `hi!!` }
* changedProperties(a, { msg: `hi!!` }); // { msg: `hi!!`, v: undefined }
* ```
*
* If B has additional or removed fields, this is considered an error.
*
* If a field is an array, the whole array is returned, rather than a diff.
* @param a
* @param b
*/
const changedProperties = (a, b) => {
const r = compareObjectData(a, b, true);
if (Object.entries(r.added).length > 0) throw new Error(`Shape of data has changed`);
if (Object.entries(r.removed).length > 0) throw new Error(`Shape of data has changed`);
return compareResultToObject(r, b);
};
const compareResultToObject = (r, b) => {
const output = {};
if (r.isArray) return b;
for (const entry of Object.entries(r.changed)) output[entry[0]] = entry[1];
for (const entry of Object.entries(r.added)) output[entry[0]] = entry[1];
for (const childEntry of Object.entries(r.children)) {
const childResult = childEntry[1];
if (childResult.hasChanged) output[childEntry[0]] = compareResultToObject(childResult, b[childEntry[0]]);
}
return output;
};
/**
* Produces a {@link CompareChangeSet} between two arrays.
*
* @param a Earlier array to compare
* @param b Later array to compare
* @param eq Equality comparison for values
* @returns Change set.
*/
const compareArrays = (a, b, eq = isEqualDefault) => {
if (!Array.isArray(a)) throw new Error(`Param 'a' is not an array`);
if (!Array.isArray(b)) throw new Error(`Param 'b' is not an array`);
const c = compareObjectData(a, b, false, eq);
if (!c.isArray) throw new Error(`Change set does not have arrays as parameters`);
const convert = (key) => {
if (key.startsWith(`_`)) return Number.parseInt(key.slice(1));
else throw new Error(`Unexpected key '${key}'`);
};
return {
...c,
added: mapObjectKeys(c.added, convert),
changed: mapObjectKeys(c.changed, convert),
removed: c.removed.map((v) => convert(v)),
summary: c.summary.map((value) => {
return [
value[0],
convert(value[1]),
value[2]
];
})
};
};
/**
* Compares A to B. Assumes they are simple objects, essentially key-value pairs, where the
* values are primitive values or other simple objects. It also works with arrays.
*
* Uses === equality semantics by default.
* @param a
* @param b
*/
const compareObjectData = (a, b, assumeSameShape = false, eq = isEqualDefault) => {
a ??= {};
b ??= {};
const entriesA = Object.entries(a);
const entriesB = Object.entries(b);
const scannedKeys = /* @__PURE__ */ new Set();
const changed = {};
const added = {};
const children = {};
const removed = [];
const isArray = Array.isArray(a);
const summary = new Array();
let hasChanged = false;
for (const entry of entriesA) {
const outputKey = isArray ? `_${entry[0]}` : entry[0];
const aValue = entry[1];
const bValue = b[entry[0]];
scannedKeys.add(entry[0]);
if (bValue === void 0) {
hasChanged = true;
if (assumeSameShape && !isArray) {
changed[outputKey] = bValue;
summary.push([
`mutate`,
outputKey,
bValue
]);
} else {
removed.push(outputKey);
summary.push([
`del`,
outputKey,
aValue
]);
}
continue;
}
if (typeof aValue === `object`) {
const r = compareObjectData(aValue, bValue, assumeSameShape, eq);
if (r.hasChanged) hasChanged = true;
children[outputKey] = r;
const childSummary = r.summary.map((sum) => {
return [
sum[0],
outputKey + `.` + sum[1],
sum[2]
];
});
summary.push(...childSummary);
} else if (!eq(aValue, bValue)) {
changed[outputKey] = bValue;
hasChanged = true;
summary.push([
`mutate`,
outputKey,
bValue
]);
}
}
if (!assumeSameShape || isArray) for (const entry of entriesB) {
const key = isArray ? `_${entry[0]}` : entry[0];
if (scannedKeys.has(entry[0])) continue;
added[key] = entry[1];
hasChanged = true;
summary.push([
`add`,
key,
entry[1]
]);
}
return {
changed,
added,
removed,
children,
hasChanged,
isArray,
summary
};
};
//#endregion
//#region ../packages/core/src/records/keys-to-numbers.ts
/**
* Returns a copy of `object` with integer numbers as keys instead of whatever it has.
* ```js
* keysToNumbers({ '1': true }); // Yields: { 1: true }
* ```
*
* The `onInvalidKey` sets how to handle keys that cannot be converted to integers.
* * 'throw' (default): throws an exception
* * 'ignore': that key & value is ignored
* * 'keep': uses the string key instead
*
*
* ```js
* keysToNumber({ hello: 'there' }, `ignore`); // Yields: { }
* keysToNumber({ hello: 'there' }, `throw`); // Exception
* keysToNumber({ hello: 'there' }, `keep`); // Yields: { hello: 'there' }
* ```
*
* Floating-point numbers will be converted to integer by rounding.
* ```js
* keysToNumbers({ '2.4': 'hello' }); // Yields: { 2: 'hello' }
* ```
* @param object
* @param onInvalidKey
* @returns
*/
const keysToNumbers = (object, onInvalidKey = `throw`) => {
const returnObject = {};
for (const entry of Object.entries(object)) {
const asNumber = Number.parseInt(entry[0]);
if (Number.isNaN(asNumber)) switch (onInvalidKey) {
case `throw`: throw new TypeError(`Cannot convert key '${entry[0]}' to an integer`);
case `ignore`: continue;
case `keep`:
returnObject[entry[0]] = entry[1];
continue;
default: throw new Error(`Param 'onInvalidKey' should be: 'throw', 'ignore' or 'keep'.`);
}
returnObject[asNumber] = entry[1];
}
return returnObject;
};
//#endregion
//#region ../packages/core/src/records/map-object.ts
/**
* Maps the top-level properties of an object through a map function.
* That is, run each of the values of an object through a function,
* setting the result onto the same key structure as original.
*
* It is NOT recursive.
*
* The mapping function gets a single args object, consisting of `{ value, field, index }`,
* where 'value' is the value of the field, 'field' the name, and 'index' a numeric count.
* @example Double the value of all fields
* ```js
* const rect = { width: 100, height: 250 };
* const doubled = mapObjectShallow(rect, args => {
* return args.value*2;
* });
* // Yields: { width: 200, height: 500 }
* ```
*
* Since the map callback gets the name of the property, it can do context-dependent things.
* ```js
* const rect = { width: 100, height: 250, colour: 'red' }
* const doubled = mapObjectShallow(rect, args => {
* if (args.field === 'width') return args.value*3;
* else if (typeof args.value === 'number') return args.value*2;
* return args.value;
* });
* // Yields: { width: 300, height: 500, colour: 'red' }
* ```
* In addition to bulk processing, it allows remapping of property types.
*
* In terms of type-safety, the mapped properties are assumed to have the
* same type.
*
* ```js
* const o = {
* x: 10,
* y: 20,
* width: 200,
* height: 200
* }
*
* // Make each property use an averager instead
* const oAvg = mapObjectShallow(o, args => {
* return movingAverage(10);
* });
*
* // Instead of { x:number, y:number... }, we now have { x:movingAverage(), y:movingAverage()... }
* // Add a value to the averager
* oAvg.x.add(20);
* ```
*/
const mapObjectShallow = (object, mapFunction) => {
const mapped = Object.entries(object).map(([sourceField, sourceFieldValue], index) => [sourceField, mapFunction({
value: sourceFieldValue,
field: sourceField,
index,
path: sourceField
})]);
return Object.fromEntries(mapped);
};
/**
* Maps the contents of `data` using `mapper` as a structured set of map functions.
* ```js
* const a = {
* person: {
* size: 20
* }
* hello: `there`
* }
* mapObjectByObject(a, {
* person: {
* size: (value, context) => {
* return value * 2
* }
* }
* });
* // Yields: { person: { size: 40 }, hello: `there` }
* ```
* @param data
* @param mapper
* @returns
*/
function mapObjectByObject(data, mapper) {
const entries = Object.entries(data);
for (const entry of entries) if (entry[0] in mapper) {
const m = mapper[entry[0]];
entry[1] = typeof m === `object` ? mapObjectByObject(entry[1], m) : m(entry[1], data);
}
return Object.fromEntries(entries);
}
//#endregion
//#region ../packages/core/src/records/merge.ts
/**
* Merges objects together, the rightmost objects overriding properties of earlier objects.
*
* The return type is the intersection of all properties
* ```js
* const a = { name: `jane`, age: 30 };
* const b = { name: `fred`, age: 31, colour: `blue` };
* const c = merge(a, b);
* // Yields:
* // { name: `fred`, age: 31, colour: `blue` }
* ```
*
* Alternatively, use {@link mergeSameShape} if the return shape
* should be based on the first object.
* @param a
* @returns Merged object
*/
function merge(...a) {
return Object.assign({}, ...a);
}
/**
* Merges objects together, conforming to the shape of the first object.
* Properties contained on later objects are ignored if they aren't part of the first.
*
* If a single object is passed in, a copy is returned.
*
* Use {@link merge} for object shape to be a union
* @param a Object arrays to merge
* @returns
*/
function mergeSameShape(...a) {
const aEntries = Object.entries(a[0]);
for (let index = 1; index < a.length; index++) {
const bEntries = Object.entries(a[index]);
for (const [key, value] of bEntries) {
const aEntry = aEntries.find(([aKey]) => aKey === key);
if (aEntry) aEntry[1] = value;
}
}
return Object.fromEntries(aEntries);
}
//#endregion
//#region ../packages/core/src/records/prefix.ts
/**
* Returns a new object based on `data` but with all
* properties prefixed by `prefix`.
*
* ```js
* prefixProperties({ name: `x`, size: 10 }, `test-`);
*
* // Yields:
* // { test-name: `x`, test-size: 10 }
* ```
* @param data
* @param prefix
* @returns
*/
function prefixProperties(data, prefix) {
const changed = [];
for (const d of data) {
const entries = Object.entries(d);
const changedEntries = [];
for (const [key, value] of entries) changedEntries.push([prefix + key, value]);
changed.push(Object.fromEntries(changedEntries));
}
return changed;
}
//#endregion
//#region ../packages/core/src/is-primitive.ts
/**
* Returns _true_ if `value` is number, string, bigint or boolean.
* Returns _false_ if `value` is an object, null, undefined
*
* Use {@link isPrimitiveOrObject} to also return true if `value` is an object.
* @param value Value to check
* @returns _True_ if value is number, string, bigint or boolean.
*/
function isPrimitive(value) {
if (typeof value === `number`) return true;
if (typeof value === `string`) return true;
if (typeof value === `bigint`) return true;
if (typeof value === `boolean`) return true;
return false;
}
/**
* Returns _true_ if `value` is number, string, bigint, boolean or an object
*
* Use {@link isPrimitive} to not include objects.
* @param value
* @returns
*/
function isPrimitiveOrObject(value) {
if (isPrimitive(value)) return true;
if (typeof value === `object`) return true;
return false;
}
//#endregion
//#region ../packages/core/src/records/traverse.ts
/**
* Helper function to get a 'friendly' string representation of an array of {@link RecordEntry}.
* @param entries
* @returns
*/
function prettyPrintEntries(entries) {
if (entries.length === 0) return `(empty)`;
let t = ``;
for (const [index, entry] of entries.entries()) {
t += ` `.repeat(index);
t += `${entry.name} = ${JSON.stringify(entry.nodeValue)}\n`;
}
return t;
}
/**
* Returns a human-friendly debug string for a tree-like structure
* ```js
* console.log(Trees.prettyPrint(obj));
* ```
* @param indent
* @param node
* @param options
* @returns
*/
function recordEntryPrettyPrint(node, indent = 0, options = {}) {
resultThrow(nullUndefTest(node, `node`));
const entry = getNamedRecordEntry(node, options.name ?? `node`);
const t = `${` `.repeat(indent)} + name: ${entry.name} value: ${JSON.stringify(entry.nodeValue)}`;
const childrenAsArray = [...recordChildren(node, options)];
return childrenAsArray.length > 0 ? `${t}\n${childrenAsArray.map((d) => recordEntryPrettyPrint(d.nodeValue, indent + 1, {
...options,
name: d.name
})).join(`\n`)}` : t;
}
/**
* Returns the direct children of a tree-like object as a pairing
* of node name and value. Supports basic objects, Maps and arrays.
*
* Sub-children are included as an object blob.
*
* @example Simple object
* ```js
* const o = {
* colour: {
* r: 0.5, g: 0.5, b: 0.5
* }
* };
*
* const children = [ ...Trees.children(o) ];
* // Children:
* // [
* // { name: "colour", value: { b: 0.5, g: 0.5, r: 0.5 } }
* // ]
* const subChildren = [ ...Trees.children(o.colour) ];
* // [ { name: "r", value: 0.5 }, { name: "g", value: 0.5 }, { name: "b", value: 0.5 } ]
* ```
*
* Arrays are assigned a name based on index.
* @example Arrays
* ```js
* const colours = [ { r: 1, g: 0, b: 0 }, { r: 0, g: 1, b: 0 }, { r: 0, g: 0, b: 1 } ];
* // Children:
* // [
* // { name: "array[0]", value: {r:1,g:0,b:0} },
* // { name: "array[1]", value: {r:0,g:1,b:0} },
* // { name: "array[2]", value: {r:0,g:0,b:1} },
* // ]
* ```
*
* Pass in `options.name` (eg 'colours') to have names generated as 'colours[0]', etc.
* Options can also be used to filter children. By default all direct children are returned.
* @param node
* @param options
*/
function* recordChildren(node, options = {}) {
resultThrow(nullUndefTest(node, `node`));
const filter = options.filter ?? `none`;
const filterByValue = (v) => {
if (filter === `none`) return [true, isPrimitive(v)];
else if (filter === `leaves` && isPrimitive(v)) return [true, true];
else if (filter === `branches` && !isPrimitive(v)) return [true, false];
return [false, isPrimitive(v)];
};
if (Array.isArray(node)) for (const [index, element] of node.entries()) {
const f = filterByValue(element);
if (f[0]) yield {
name: index.toString(),
sourceValue: element,
nodeValue: f[1] ? element : void 0
};
}
else if (typeof node === `object`) if (options.withPrototype) for (const name in node) {
const value = node[name];
const f = filterByValue(value);
if (f[0]) yield {
name,
sourceValue: value,
nodeValue: f[1] ? value : void 0
};
}
else {
const entriesIter = `entries` in node ? node.entries() : Object.entries(node);
for (const [name, value] of entriesIter) {
const f = filterByValue(value);
if (f[0]) yield {
name,
sourceValue: value,
nodeValue: f[1] ? value : void 0
};
}
}
}
function* recordEntriesDepthFirst(node, options = {}, ancestors = []) {
if (node === null || node === void 0) return;
const opts = {
filter: options.filter ?? `none`,
name: options.name ?? ``,
withPrototype: options.withPrototype ?? false,
seen: options.seen ?? /* @__PURE__ */ new WeakSet()
};
if (typeof node === `object`) {
if (opts.seen.has(node)) return;
opts.seen.add(node);
}
for (const c of recordChildren(node, opts)) {
yield {
...c,
ancestors: [...ancestors]
};
yield* recordEntriesDepthFirst(c.sourceValue, opts, [...ancestors, c.name]);
}
}
/**
* Finds a given direct child by name
* @param name
* @param node
* @returns
*/
function recordEntryChildByName(name, node) {
for (const d of recordChildren(node)) if (d.name === name) return d;
}
/**
* Returns the closest matching entry, tracing `path` in an array, Map or simple object.
* Returns an entry with _undefined_ value at the point where tracing stopped.
* Use {@link traceRecordEntryByPath} to step through all the segments.
*
* ```js
* const people = {
* jane: {
* address: {
* postcode: 1000,
* street: 'West St',
* city: 'Blahville'
* },
* colour: 'red'
* }
* }
* Trees.getByPath('jane.address.postcode', people); // '.' default separator
* // ['postcode', 1000]
* Trees.getByPath('jane.address.country.state', people);
* // ['country', undefined] - since full path could not be resolved.
* ```
* @param path Path, eg `jane.address.postcode`
* @param node Node to look within
* @param options Options for parsing path. By default '.' is used as a separator
* @returns
*/
function getRecordEntryByPath(path, node, options = {}) {
const paths = [...traceRecordEntryByPath(path, node, options)];
if (paths.length === 0) throw new Error(`Could not trace path: ${path} `);
return paths.at(-1);
}
/**
* Enumerates over children of `node` towards the node named in `path`.
* This is useful if you want to get the interim steps to the target node.
*
* Use {@link getRecordEntryByPath} if you don't care about interim steps.
*
* ```js
* const people = {
* jane: {
* address: {
* postcode: 1000,
* street: 'West St',
* city: 'Blahville'
* },
* colour: 'red'
* }
* }
* for (const p of Trees.traceByPath('jane.address.street', people)) {
* // { name: "jane", value: { address: { postcode: 1000,street: 'West St', city: 'Blahville' }, colour: 'red'} },
* // { name: "address", value: { postcode: 1000, street: 'West St', city: 'Blahville' } },
* // { name: "street", value: "West St" } }
* }
* ```
*
* Results stop when the path can't be followed any further.
* The last entry will have a name of the last sought path segment, and _undefined_ as its value.
*
* @param path Path to traverse
* @param node Starting node
* @param options Options for path traversal logic
* @returns
*/
function* traceRecordEntryByPath(path, node, options = {}) {
resultThrow(nullUndefTest(path, `path`), nullUndefTest(node, `node`));
const separator = options.separator ?? `.`;
const pathSplit = path.split(separator);
const ancestors = [];
for (const p of pathSplit) {
const entry = recordEntryChildByName(p, node);
if (!entry) {
yield {
name: p,
sourceValue: void 0,
nodeValue: void 0,
ancestors
};
return;
}
node = entry.sourceValue;
yield {
...entry,
ancestors: [...ancestors]
};
ancestors.push(p);
}
}
/**
* Generates a name for a node.
* Uses the 'name' property if it exists, otherwise uses `defaultName`
* @param node
* @param defaultName
* @returns
*/
function getNamedRecordEntry(node, defaultName = ``) {
if (`name` in node && `nodeValue` in node && `sourceValue` in node) return node;
if (`name` in node) return {
name: node.name,
nodeValue: node,
sourceValue: node
};
return {
name: defaultName,
nodeValue: node,
sourceValue: node
};
}
//#endregion
//#region ../packages/core/src/records/values.ts
/**
* Yields numerical values based on the an input of objects and the property to use.
*
* ```js
* const data = [
* { size: 10 }, { size: 20 }, { size: 0 }
* ]
* [...enumerateNumericalValues(data, `size`)]; // [ 10, 20, 0 ]
* ```
*
* If any of objects has a non numerical value for `propertyName`, a TypeError is thrown.
* @param records
* @param propertyName
* @returns
*/
function* enumerateNumericalValues(records, propertyName) {
for (const rec of records) {
const fieldValue = rec[propertyName];
if (typeof fieldValue !== `number`) throw new TypeError(`Property value was not a number. Property: ${propertyName} Value: ${fieldValue} Type: ${typeof fieldValue}`);
yield fieldValue;
}
}
//#endregion
//#region ../packages/core/src/records/zip.ts
/**
* Merge corresponding objects from arrays. This is assuming objects at the same array indices are connected.
*
* Arrays must be the same length. When merging objects, the properties of later objects override those of earlier objects.
*
* ```js
* const a = [ { name: `jane`, age: 30 }, { name: `bob`, age: 40 } ];
* const b = [ { name: `fred`, colour: `red` }, { name: `johanne` } ];
* const c = [...zip(a, b)];
* // Yields:
* // [
* // { name: `fred`, age: 30, colour: `red` },
* // { name: `johanne`, age: 40 }
* // ]
* ```
* @param toMerge Arrays to merge
* @throws {TypeError} If either parameter is not an array
* @throws {TypeError} If the arrays are not of the same length
* @returns Generator of merged records
*/
function* zip$1(...toMerge) {
let len = -1;
for (let index = 0; index < toMerge.length; index++) {
if (!Array.isArray(toMerge[index])) throw new TypeError(`Value at index ${index} is not an array as expected`);
if (len === -1) len = toMerge[index].length;
else if (toMerge[index].length !== len) throw new TypeError(`Array length mismatch. Expected: ${len} Actual: ${toMerge[index].length} Array: ${index}`);
}
for (let index = 0; index < len; index++) yield merge(...toMerge.map((arr) => arr[index]));
}
//#endregion
//#region ../packages/core/src/records.ts
var records_exports = /* @__PURE__ */ __exportAll({
changedProperties: () => changedProperties,
cloneFromFields: () => cloneFromFields,
compareArrays: () => compareArrays,
compareObjectData: () => compareObjectData,
compareObjectKeys: () => compareObjectKeys,
enumerateNumericalValues: () => enumerateNumericalValues,
getRecordEntryByPath: () => getRecordEntryByPath,
keysToNumbers: () => keysToNumbers,
mapObjectByObject: () => mapObjectByObject,
mapObjectKeys: () => mapObjectKeys,
mapObjectShallow: () => mapObjectShallow,
merge: () => merge,
mergeSameShape: () => mergeSameShape,
prefixProperties: () => prefixProperties,
prettyPrintEntries: () => prettyPrintEntries,
recordChildren: () => recordChildren,
recordEntriesDepthFirst: () => recordEntriesDepthFirst,
recordEntryPrettyPrint: () => recordEntryPrettyPrint,
removeCircularReferences: () => removeCircularReferences,
traceRecordEntryByPath: () => traceRecordEntryByPath,
zip: () => zip$1
});
//#endregion
//#region ../packages/core/src/to-string.ts
const objectToString = Object.prototype.toString;
function toTypeString(value) {
return objectToString.call(value);
}
/**
* Returns _true_ if `value` is a Map type
* @param value
* @returns
*/
function isMap(value) {
return toTypeString(value) === `[object Map]`;
}
/**
* Returns _true_ if `value` is a Set type
* @param value
* @returns
*/
function isSet(value) {
return toTypeString(value) === `[object Set]`;
}
/**
* A default converter to string that uses JSON.stringify if its an object, or the thing itself if it's a string
*/
function toStringDefault(itemToMakeStringFor) {
return typeof itemToMakeStringFor === `string` ? itemToMakeStringFor : JSON.stringify(itemToMakeStringFor);
}
/**
* Converts a value to string form.
* For simple objects, .toString() is used, other JSON.stringify is used.
* It is meant for creating debugging output or 'hash' versions of objects, and does
* not necessarily maintain full fidelity of the input
* @param value
* @returns
*/
function defaultToString(value) {
if (value === null) return `null`;
if (typeof value === `boolean` || typeof value === `number`) return value.toString();
if (typeof value === `string`) return value;
if (typeof value === `symbol`) throw new TypeError(`Symbol cannot be converted to string`);
try {
return JSON.stringify(value);
} catch (error) {
if (typeof value === `object`) return JSON.stringify(removeCircularReferences(value, `(circular)`));
else throw error;
}
}
/**
* If input is a string, it is returned.
* Otherwise, it returns the result of JSON.stringify() with fields ordered.
*
* This allows for more consistent comparisons when object field orders are different but values the same.
* @param itemToMakeStringFor
*/
function toStringOrdered(itemToMakeStringFor) {
if (typeof itemToMakeStringFor === `string`) return itemToMakeStringFor;
const replacer = (key, value) => value instanceof Object && !Array.isArray(value) ? Object.keys(value).sort().reduce((sorted, key) => {
sorted[key] = value[key];
return sorted;
}, {}) : value;
return JSON.stringify(itemToMakeStringFor, replacer);
}
//#endregion
//#region ../packages/core/src/comparers.ts
/**
* Sort numbers in ascending order.
*
* ```js
* [10, 4, 5, 0].sort(numericComparer);
* // Yields: [0, 4, 5, 10]
* [10, 4, 5, 0].sort(comparerInverse(numericComparer));
* // Yields: [ 10, 5, 4, 0]
* ```
*
* Returns:
* 0: values are equal
* negative: `a` should be before `b`
* positive: `a` should come after `b`
* @param a
* @param b
* @returns
*/
function numericComparer(a, b) {
if (a === b) return 0;
if (a > b) return 1;
return -1;
}
/**
* Default sort comparer, following same sematics as Array.sort.
* Consider using {@link defaultComparer} to get more logical sorting of numbers.
*
* Note: numbers are sorted in alphabetical order, eg:
* ```js
* [ 10, 20, 5, 100 ].sort(jsComparer); // same as .sort()
* // Yields: [10, 100, 20, 5]
* ```
*
* Returns -1 if x is less than y
* Returns 1 if x is greater than y
* Returns 0 if x is the same as y
* @param x
* @param y
* @returns
*/
function jsComparer(x, y) {
if (x === void 0 && y === void 0) return 0;
if (x === void 0) return 1;
if (y === void 0) return -1;
const xString = defaultToString(x);
const yString = defaultToString(y);
if (xString < yString) return -1;
if (xString > yString) return 1;
return 0;
}
/**
* Inverts the source comparer.
* @param comparer
* @returns
*/
function comparerInverse(comparer) {
return (x, y) => {
return comparer(x, y) * -1;
};
}
/**
* Compares numbers by numeric value, otherwise uses the default
* logic of string comparison.
*
* Is an ascending sort:
* b, a, c -> a, b, c
* 10, 5, 100 -> 5, 10, 100
*
* Returns -1 if x is less than y
* Returns 1 if x is greater than y
* Returns 0 if x is the same as y
* @param x
* @param y
* @see {@link comparerInverse} Inverted order
* @returns
*/
function defaultComparer(x, y) {
if (typeof x === `number` && typeof y === `number`) return numericComparer(x, y);
return jsComparer(x, y);
}
//#endregion
//#region ../packages/core/src/is-equal.ts
/**
* Default comparer function is equiv to checking `a === b`.
* Use {@link isEqualValueDefault} to compare by value, via comparing JSON string representation.
*/
const isEqualDefault = (a, b) => a === b;
/**
* Comparer returns true if string representation of `a` and `b` are equal.
* Use {@link isEqualDefault} to compare using === semantics
* Uses `toStringDefault` to generate a string representation (via `JSON.stringify`).
*
* Returns _false_ if the ordering of fields is different, even though values are identical:
* ```js
* isEqualValueDefault({ a: 10, b: 20}, { b: 20, a: 10 }); // false
* ```
*
* Use {@link isEqualValueIgnoreOrder} to ignore order (with an overhead of additional processing).
* ```js
* isEqualValueIgnoreOrder({ a: 10, b: 20}, { b: 20, a: 10 }); // true
* ```
*
* Use {@link isEqualValuePartial} to partially match `b` against `a`.
* @returns True if the contents of `a` and `b` are equal
*/
function isEqualValueDefault(a, b) {
if (a === b) return true;
return toStringDefault(a) === toStringDefault(b);
}
/**
* Returns _true_ if `a` contains the values of `b`. `a` may contain other values, but we
* only check against what is in `b`. `a` and `b` must both be simple objects.
*
* ```js
* const obj = {
* name: `Elle`,
* size: 100,
* colour: {
* red: 0.5,
* green: 0.1,
* blue: 0.2
* }
* }
*
* isEqualValuePartial(obj, { name: `Elle` }); // true
* isEqualValuePartial(obj, { name: { colour: red: { 0.5, green: 0.1 }} }); // true
*
* isEqualValuePartial(obj, { name: `Ellen` }); // false
* isEqualValuePartial(obj, { lastname: `Elle` }); // false
* ```
* @param a
* @param b
* @param fieldComparer
* @returns
*/
function isEqualValuePartial(a, b, fieldComparer) {
if (typeof a !== `object`) throw new Error(`Param 'a' expected to be object`);
if (typeof b !== `object`) throw new Error(`Param 'b' expected to be object`);
if (Object.is(a, b)) return true;
const comparer = fieldComparer ?? isEqualValuePartial;
for (const entryB of Object.entries(b)) {
const valueOnAKeyFromB = a[entryB[0]];
const valueB = entryB[1];
if (typeof valueOnAKeyFromB === `object` && typeof valueB === `object`) {
if (!comparer(valueOnAKeyFromB, valueB)) return false;
} else if (valueOnAKeyFromB !== valueB) return false;
}
return true;
}
/**
* Comparer returns true if string representation of `a` and `b` are equal, regardless of field ordering.
* Uses `toStringOrdered` to generate a string representation (via JSON.stringify`).
*
* ```js
* isEqualValueIgnoreOrder({ a: 10, b: 20}, { b: 20, a: 10 }); // true
* isEqualValue({ a: 10, b: 20}, { b: 20, a: 10 }); // false, fields are different order
* ```
*
* There is an overhead to ordering fields. Use {@link isEqualValueDefault} if it's not possible that field ordering will change.
* @returns True if the contents of `a` and `b` are equal
* @typeParam T - Type of objects being compared
*/
function isEqualValueIgnoreOrder(a, b) {
if (a === b) return true;
return toStringOrdered(a) === toStringOrdered(b);
}
/**
* Returns _true_ if Object.entries() is empty for `value`
* @param value
* @returns
*/
const isEmptyEntries = (value) => [...Object.entries(value)].length === 0;
/**
* Returns _true_ if `a` and `b` are equal based on their JSON representations.
* `path` is ignored.
* @param a
* @param b
* @param path
* @returns
*/
const isEqualContextString = (a, b, _path) => {
return JSON.stringify(a) === JSON.stringify(b);
};
//#endregion
//#region ../packages/core/src/maps.ts
var maps_exports = /* @__PURE__ */ __exportAll({
addObjectEntriesMutate: () => addObjectEntriesMutate,
addValue: () => addValue,
addValueMutate: () => addValueMutate,
addValueMutator: () => addValueMutator,
deleteByValueCompareMutate: () => deleteByValueCompareMutate,
filterValues: () => filterValues,
findBySomeKey: () => findBySomeKey,
findEntryByPredicate: () => findEntryByPredicate,
findEntryByValue: () => findEntryByValue,
findValue: () => findValue,
fromIterable: () => fromIterable,
fromObject: () => fromObject,
getClosestIntegerKey: () => getClosestIntegerKey,
getOrGenerate: () => getOrGenerate,
getOrGenerateSync: () => getOrGenerateSync,
hasAnyValue: () => hasAnyValue,
hasKeyValue: () => hasKeyValue,
mapToArray: () => mapToArray,
mapToObjectTransform: () => mapToObjectTransform,
mergeByKey: () => mergeByKey,
some: () => some,
sortByValue: () => sortByValue,
sortByValueProperty: () => sortByValueProperty,
toArray: () => toArray,
toObject: () => toObject,
transformMap: () => transformMap,
zipKeyValue: () => zipKeyValue
});
/**
* Gets the closest integer key to `target` in `data`.
* * Requires map to have numbers as keys, not strings
* * Math.round is used for rounding `target`.
*
* Examples:
* ```js
* // Assuming numeric keys 1, 2, 3, 4 exist:
* getClosestIntegerKey(map, 3); // 3
* getClosestIntegerKey(map, 3.1); // 3
* getClosestIntegerKey(map, 3.5); // 4
* getClosestIntegerKey(map, 3.6); // 4
* getClosestIntegerKey(map, 100); // 4
* getClosestIntegerKey(map, -100); // 1
* ```
* @param data Map
* @param target Target value
* @returns
*/
const getClosestIntegerKey = (data, target) => {
target = Math.round(target);
if (data.has(target)) return target;
else {
let offset = 1;
while (offset < 1e3) {
if (data.has(target - offset)) return target - offset;
else if (data.has(target + offset)) return target + offset;
offset++;
}
throw new Error(`Could not find target ${target.toString()}`);
}
};
/**
* Returns the first value in `data` that matches a key from `keys`.
* ```js
* // Iterate, yielding: `a.b.c.d`, `b.c.d`, `c.d`, `d`
* const keys = Text.segmentsFromEnd(`a.b.c.d`);
* // Gets first value that matches a key (starting from most precise)
* const value = findBySomeKey(data, keys);
* ```
* @param data
* @param keys
* @returns
*/
const findBySomeKey = (data, keys) => {
for (const key of keys) if (data.has(key)) return data.get(key);
};
/**
* Returns true if map contains `value` under `key`, using `comparer` function. Use {@link hasAnyValue} if you don't care
* what key value might be under.
*
* Having a comparer function is useful to check by value rather than object reference.
*
* @example Find key value based on string equality
* ```js
* hasKeyValue(map,`hello`, `samantha`, (a, b) => a === b);
* ```
* @param map Map to search
* @param key Key to search
* @param value Value to search
* @param comparer Function to determine match. By default uses === comparison.
* @returns True if key is found
*/
const hasKeyValue = (map, key, value, comparer = isEqualDefault) => {
if (!map.has(key)) return false;
return [...map.values()].some((v) => comparer(v, value));
};
/**
* Deletes all key/values from map where value matches `value`,
* with optional comparer. Mutates map.
*
* ```js
* // Compare fruits based on their colour property
* const colourComparer = (a, b) => a.colour === b.colour;
*
* // Deletes all values where .colour = `red`
* deleteByValueCompareMutate(map, { colour: `red` }, colourComparer);
* ```
* @param map
* @param value
* @param comparer Uses === equality by default. Use isEqualValueDefault to compare by value
*/
const deleteByValueCompareMutate = (map, value, comparer = isEqualDefault) => {
for (const entry of map.entries()) if (comparer(entry[1], value)) map.delete(entry[0]);
};
/**
* Finds first entry by iterable value. Expects a map with an iterable as values.
*
* ```js
* const map = new Map();
* map.set('hello', 'a');
* map.set('there', 'b');
*
* const entry = findEntryByPredicate(map, (value, key) => {
* return (value === 'b');
* });
* // Entry is: ['there', 'b']
* ```
*
* An alternative is {@link findEntryByValue} to search by value.
* @param map Map to search
* @param predicate Filter function returns true when there is a match of value
* @returns Entry, or _undefined_ if `filter` function never returns _true_
*/
const findEntryByPredicate = (map, predicate) => {
for (const entry of map.entries()) if (predicate(entry[1], entry[0])) return entry;
};
/**
* Finds first entry by value.
*
* ```js
* const map = new Map();
* map.set('hello', 'a');
* map.set('there', 'b');
*
* const entry = findEntryByValue(map, 'b');
* // Entry is: ['there', 'b']
* ```
*
* Uses JS's === comparison by default. Consider using `isEqualValueDefault` to match by value.
* An alternative is {@link findEntryByValue} to search by predicate function.
* @param map Map to search
* @param value Value to seek
* @param isEqual Filter function which checks equality. Uses JS comparer by default.
* @returns Entry, or _undefined_ if `value` not found.
*/
const findEntryByValue = (map, value, isEqual = isEqualDefault) => {
for (const entry of map.entries()) if (isEqual(entry[1], value)) return entry;
};
/**
* Adds items to a map only if their key doesn't already exist
*
* Uses provided {@link ToString} function to create keys for items. Item is only added if it doesn't already exist.
* Thus the older item wins out, versus normal `Map.set` where the newest wins.
*
* Returns a copy of the input map.
* @example
* ```js
* const map = new Map();
* const peopleArray = [ _some people objects..._];
* addKeepingExisting(map, p => p.name, ...peopleArray);
* ```
* @param set
* @param hasher
* @param values
* @returns
*/
/**
* Mutates `map`, adding each value to it using a
* function to produce a key. Use {@link addValue} for an immutable version.
* ```
* const map = new Map();
* addValueMutate(map, v=>v.name, { name:`Jane`, size:10 }, { name:`Bob`, size: 9 });
* // Map consists of entries:
* // [ `Jane`, { name:`Jane`, size:10 } ],
* // [ `Bob` { name:`Bob`, size: 9 } ]
* ```
*
* Uses {@link addValueMutator} under the hood.
* @param map Map to modify. If _undefined_, a new map is created
* @param hasher Function to generate a string key for a given object value
* @param values Values to add
* @param collisionPolicy What to do if the key already exists
* @returns Map instance
*/
const addValueMutate = (map, hasher, collisionPolicy, ...values) => {
const m = map ?? /* @__PURE__ */ new Map();
addValueMutator(m, hasher, collisionPolicy)(...values);
return m;
};
/**
* Adds values to a map, returning a new, modified copy and leaving the original
* intact.
*
* Use {@link addValueMutate} for a mutable
* @param map Map to start with, or _undefined_ to automatically create a map
* @param hasher Function to create keys for values
* @param collisionPolicy What to do if a key already exists
* @param values Values to add
* @returns A new map containing values
*/
const addValue = (map, hasher, collisionPolicy, ...values) => {
const m = map === void 0 ? /* @__PURE__ */ new Map() : new Map(map);
for (const v of values) {
const hashResult = hasher(v);
if (collisionPolicy !== `overwrite`) {
if (m.has(hashResult)) {
if (collisionPolicy === `throw`) throw new Error(`Key '${hashResult}' already in map`);
if (collisionPolicy === `skip`) continue;
}
}
m.set(hashResult, v);
}
return m;
};
/**
* Returns a function that adds values to a map, using a hashing function to produce a key.
* Use {@link addValueMutate} if you don't need a reusable function.
*
* ```js
* const map = new Map(); // Create map
* const mutate = addValueMutator(map, v=>v.name); // Create a mutator using default 'overwrite' policy
* mutate( { name:`Bob`, size:10 }, { name: `Alice`, size: 2 }); // Add values to map
* mutate( {name: `Bob`, size: 11 }); // Change the value stored under key `Bob`.
* map.get(`Bob`); // { name: `Bob`, size: 11 }
* ```
*
* The 'collision policy' determines what to do if the key already exists. The default behaviour
* is to overwrite the key, just as Map.set would.
* ```js
* const map = new Map();
* const mutate = addValueMutator(map, v=>v.name, `skip`);
* mutate( { name:`Bob`,size:10 }, { name: `Alice`, size: 2 }); // Add values to map
* mutate( { name:`Bob`, size: 20 }); // This value would be skipped because map already contains 'Bob'
* map.get(`Bob`); // { name: `Bob`, size: 10 }
* ```
*
* @param map Map to modify
* @param hasher Hashing function to make a key for a value
* @param collisionPolicy What to do if a value is already stored under a key
* @returns Function
*/
const addValueMutator = (map, hasher, collisionPolicy = `overwrite`) => {
return (...values) => {
for (const v of values) {
const hashResult = hasher(v);
if (collisionPolicy !== `overwrite`) {
if (map.has(hashResult)) {
if (collisionPolicy === `throw`) throw new Error(`Key '${hashResult}' already in map`);
if (collisionPolicy === `skip`) continue;
}
}
map.set(hashResult, v);
}
return map;
};
};
/**
* Returns a array of entries from a map, sorted by value.
*
* ```js
* const m = new Map();
* m.set(`4491`, { name: `Bob` });
* m.set(`2319`, { name: `Alice` });
*
* // Compare by name
* const comparer = (a, b) => defaultComparer(a.name, b.name);
*
* // Get sorted values
* const sorted = Maps.sortByValue(m, comparer);
* ```
*
* `sortByValue` takes a comparison function that should return -1, 0 or 1 to indicate order of `a` to `b`.
* @param map
* @param comparer
* @returns
*/
const sortByValue = (map, comparer) => {
const f = comparer ?? defaultComparer;
return [...map.entries()].sort((a, b) => f(a[1], b[1]));
};
/**
* Returns an array of entries from a map, sorted by a property of the value
*
* ```js
* const m = new Map();
* m.set(`4491`, { name: `Bob` });
* m.set(`2319`, { name: `Alice` });
* const sorted = sortByValueProperty(m, `name`);
* ```
* @param map Map to sort
* @param property Property of value
* @param compareFunction Comparer. If unspecified, uses a default.
*/
const sortByValueProperty = (map, property, compareFunction) => {
const cfn = typeof compareFunction === `undefined` ? defaultComparer : compareFunction;
return [...map.entries()].sort((aE, bE) => {
const a = aE[1];
const b = bE[1];
return cfn(a[property], b[property]);
});
};
/**
* Returns _true_ if any key contains `value`, based on the provided `comparer` function. Use {@link hasKeyValue}
* if you only want to find a value under a certain key.
*
* Having a comparer function is useful to check by value rather than object reference.
* @example Finds value where name is 'samantha', regardless of other properties
* ```js
* hasAnyValue(map, {name:`samantha`}, (a, b) => a.name === b.name);
* ```
*
* Works by comparing `value` against all values contained in `map` for equality using the provided `comparer`.
*
* @param map Map to search
* @param value Value to find
* @param comparer Function that determines matching. Should return true if `a` and `b` are considered equal.
* @returns True if value is found
*/
const hasAnyValue = (map, value, comparer) => {
return [...map.entries()].some((kv) => comparer(kv[1], value));
};
/**
* Returns values where `predicate` returns true.
*
* If you just want the first match, use `find`
*
* @example All people over thirty
* ```js
* // for-of loop
* for (const v of filterValues(people, person => person.age > 30)) {
*
* }
* // If you want an array
* const overThirty = Array.from(filterValues(people, person => person.age > 30));
* ```
* @param map Map
* @param predicate Filtering predicate
* @returns Values that match predicate
*/
function* filterValues(map, predicate) {
for (const v of map.values()) if (predicate(v)) yield v;
}
/**
* Copies data to an array
* @param map
* @returns
*/
const toArray = (map) => [...map.values()];
/**
* Returns a Map from an iterable. By default throws an exception
* if iterable contains duplicate values.
*
* ```js
* const data = [
* { fruit: `granny-smith`, family: `apple`, colour: `green` },
* { fruit: `mango`, family: `stone-fruit`, colour: `orange` }
* ];
* const map = fromIterable(data, v => v.fruit);
* map.get(`granny-smith`); // { fruit: `granny-smith`, family: `apple`, colour: `green` }
* ```
* @param data Input data
* @param keyFunction Function which returns a string id. By default uses the JSON value of the object.
* @param collisionPolicy By default, values with same key overwrite previous (`overwrite`)
* @returns
*/
const fromIterable = (data, keyFunction = toStringDefault, collisionPolicy = `overwrite`) => {
const m = /* @__PURE__ */ new Map();
for (const d of data) {
const key = keyFunction(d);
if (m.has(key)) {
if (collisionPolicy === `throw`) throw new Error(`Key '${key}' is already used and new data will overwrite it. `);
if (collisionPolicy === `skip`) continue;
}
m.set(key, d);
}
return m;
};
/**
* Returns a Map from an object, or array of objects.
* Assumes the top-level properties of the object is the key.
*
* ```js
* const data = {
* Sally: { name: `Sally`, colour: `red` },
* Bob: { name: `Bob`, colour: `pink` }
* };
* const map = fromObject(data);
* map.get(`Sally`); // { name: `Sally`, colour: `red` }
* ```
*
* To add an object to an existing map, use {@link addObjectEntriesMutate}.
* @param data
* @returns
*/
const fromObject = (data) => {
const map = /* @__PURE__ */ new Map();
if (Array.isArray(data)) for (const d of data) addObjectEntriesMutate(map, d);
else addObjectEntriesMutate(map, data);
return map;
};
/**
* Adds an object to an existing map, mutating it.
* It assumes a structure where each top-level property is a key:
*
* ```js
* const data = {
* Sally: { colour: `red` },
* Bob: { colour: `pink` }
* };
* const map = new Map();
* addObjectEntriesMutate(map, data);
*
* map.get(`Sally`); // { name: `Sally`, colour: `red` }
* ```
*
* To create a new map from an object, use {@link fromObject} instead.
* @param map
* @param data
*/
const addObjectEntriesMutate = (map, data) => {
const entries = Object.entries(data);
for (const [key, value] of entries) map.set(key, value);
};
/**
* Returns the first found value that matches `predicate` or _undefined_.
* To get an entry see {@link findEntryByPredicate}
*
* Use {@link some} if you don't care about the value, just whether it appears.
* Use {@link filterValues} to get all value(s) that match `predicate`.
*
* @example First person over thirty
* ```js
* const overThirty = findValue(people, person => person.age > 30);
* ```
* @param map Map to search
* @param predicate Function that returns true for a matching value
* @returns Found value or _undefined_
*/
const findValue = (map, predicate) => [...map.values()].find((v) => predicate(v));
/**
* Returns _true_ if `predicate` yields _true_ for any value in `map`.
* Use {@link findValue} if you want the matched value.
* ```js
* const map = new Map();
* map.set(`fruit`, `apple`);
* map.set(`colour`, `red`);
* Maps.some(map, v => v === `red`); // true
* Maps.some(map, v => v === `orange`); // false
* ```
* @param map
* @param predicate
* @returns
*/
const some = (map, predicate) => [...map.values()].some((v) => predicate(v));
/**
* Converts a map to a simple object, transforming from type `T` to `K` as it does so. If no transforms are needed, use {@link toObject}.
*
* ```js
* const map = new Map();
* map.set(`name`, `Alice`);
* map.set(`pet`, `dog`);
*
* const o = mapToObjectTransform(map, v => {
* ...v,
* registered: true
* });
*
* // Yields: { name: `Alice`, pet: `dog`, registered: true }
* ```
*
* If the goal is to create a new map with transformed values, use {@link transformMap}.
* @param m
* @param valueTransform
* @typeParam T Value type of input map
* @typeParam K Value type of destination map
* @returns
*/
const mapToObjectTransform = (m, valueTransform) => [...m].reduce((object, [key, value]) => {
object[key] = valueTransform(value);
return object;
}, {});
/**
* Zips together an array of keys and values into an object. Requires that
* `keys` and `values` are the same length.
*
* @example
* ```js
* const o = zipKeyValue([`a`, `b`, `c`], [0, 1, 2])
* Yields: { a: 0, b: 1, c: 2}
*```
* @param keys String keys
* @param values Values
* @typeParam V Type of values
* @return Object with keys and values
*/
const zipKeyValue = (keys, values) => {
if (keys.length !== values.length) throw new Error(`Keys and values arrays should be same length`);
return Object.fromEntries(keys.map((k, index) => [k, values[index]]));
};
/**
* Like `Array.map`, but for a Map. Transforms from Map<K,V> to Map<K,R>, returning as a new Map.
*
* @example
* ```js
* const mapOfStrings = new Map();
* mapOfStrings.set(`a`, `10`);
* mapOfStrings.get(`a`); // Yields `10` (a string)
*
* // Convert a map of string->string to string->number
* const mapOfInts = transformMap(mapOfStrings, (value, key) => parseInt(value));
*
* mapOfInts.get(`a`); // Yields 10 (a proper number)
* ```
*
* If you want to combine values into a single object, consider instead {@link mapToObjectTransform}.
* @param source
* @param transformer
* @typeParam K Type of keys (generally a string)
* @typeParam V Type of input map values
* @typeParam R Type of output map values
* @returns
*/
const transformMap = (source, transformer) => new Map(Array.from(source, (v) => [v[0], transformer(v[1], v[0])]));
/**
* Converts a `Map` to a plain object, useful for serializing to JSON.
* To convert back to a map use {@link fromObject}.
*
* @example
* ```js
* const map = new Map();
* map.set(`Sally`, { name: `Sally`, colour: `red` });
* map.set(`Bob`, { name: `Bob`, colour: `pink });
*
* const objects = Maps.toObject(map);
* // Yields: {
* // Sally: { name: `Sally`, colour: `red` },
* // Bob: { name: `Bob`, colour: `pink` }
* // }
* ```
* @param m
* @returns
*/
const toObject = (m) => [...m].reduce((object, [key, value]) => {
object[key] = value;
return object;
}, {});
/**
* Converts Map to Array with a provided `transformer` function. Useful for plucking out certain properties
* from contained values and for creating a new map based on transformed values from an input map.
*
* @example Get an array of ages from a map of Person objects
* ```js
* const person = { age: 29, name: `John`};
* map.set(person.name, person);
*
* const ages = mapToArray(map, (key, person) => person.age);
* // [29, ...]
* ```
*
* In the