UNPKG

ixfx

Version:

Bundle of ixfx libraries

1,631 lines (1,614 loc) 112 kB
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