diff options
Diffstat (limited to 'src/util/sugar.js')
-rw-r--r-- | src/util/sugar.js | 867 |
1 files changed, 669 insertions, 198 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js index 38c8047..e060f45 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -6,267 +6,738 @@ // It will likely only do exactly what I want it to, and only in the cases I // decided were relevant enough to 8other handling. +import {colors} from './cli.js'; + // Apparently JavaScript doesn't come with a function to split an array into // chunks! Weird. Anyway, this is an awesome place to use a generator, even // though we don't really make use of the 8enefits of generators any time we // actually use this. 8ut it's still awesome, 8ecause I say so. export function* splitArray(array, fn) { - let lastIndex = 0; - while (lastIndex < array.length) { - let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item)); - if (nextIndex === -1) { - nextIndex = array.length; - } - yield array.slice(lastIndex, nextIndex); - // Plus one because we don't want to include the dividing line in the - // next array we yield. - lastIndex = nextIndex + 1; + let lastIndex = 0; + while (lastIndex < array.length) { + let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item)); + if (nextIndex === -1) { + nextIndex = array.length; + } + yield array.slice(lastIndex, nextIndex); + // Plus one because we don't want to include the dividing line in the + // next array we yield. + lastIndex = nextIndex + 1; + } +} + +// Null-accepting function to check if an array or set is empty. Accepts null +// (which is treated as empty) as a shorthand for "hey, check if this property +// is an array with/without stuff in it" for objects where properties that are +// PRESENT but don't currently have a VALUE are null (rather than undefined). +export function empty(value) { + if (value === null) { + return true; + } + + if (Array.isArray(value)) { + return value.length === 0; + } + + if (value instanceof Set) { + return value.size === 0; + } + + throw new Error(`Expected array, set, or null`); +} + +// Repeats all the items of an array a number of times. +export function repeat(times, array) { + if (typeof array === 'string') return repeat(times, [array]); + if (empty(array)) return []; + if (times === 0) return []; + if (times === 1) return array.slice(); + + const out = []; + for (let n = 1; n <= times; n++) { + out.push(...array); + } + return out; +} + +// Gets the item at an index relative to another index. +export function atOffset(array, index, offset, { + wrap = false, + valuePastEdge = null, +} = {}) { + if (index === -1) { + return valuePastEdge; + } + + if (offset === 0) { + return array[index]; + } + + if (wrap) { + return array[(index + offset) % array.length]; + } + + if (offset > 0 && index + offset > array.length - 1) { + return valuePastEdge; + } + + if (offset < 0 && index + offset < 0) { + return valuePastEdge; + } + + return array[index + offset]; +} + +// Sums the values in an array, optionally taking a function which maps each +// item to a number (handy for accessing a certain property on an array of like +// objects). This also coalesces null values to zero, so if the mapping function +// returns null (or values in the array are nullish), they'll just be skipped in +// the sum. +export function accumulateSum(array, fn = x => x) { + return array.reduce( + (accumulator, value, index, array) => + accumulator + + fn(value, index, array) ?? 0, + 0); +} + +// Stitches together the items of separate arrays into one array of objects +// whose keys are the corresponding items from each array at that index. +// This is mostly useful for iterating over multiple arrays at once! +export function stitchArrays(keyToArray) { + const errors = []; + + for (const [key, value] of Object.entries(keyToArray)) { + if (value === null) continue; + if (Array.isArray(value)) continue; + errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`)); + } + + if (!empty(errors)) { + throw new AggregateError(errors, `Expected arrays or null`); + } + + const keys = Object.keys(keyToArray); + const arrays = Object.values(keyToArray).filter(val => Array.isArray(val)); + const length = Math.max(...arrays.map(({length}) => length)); + const results = []; + + for (let i = 0; i < length; i++) { + const object = {}; + for (const key of keys) { + object[key] = + (Array.isArray(keyToArray[key]) + ? keyToArray[key][i] + : null); + } + results.push(object); + } + + return results; +} + +// Turns this: +// +// [ +// [123, 'orange', null], +// [456, 'apple', true], +// [789, 'banana', false], +// [1000, 'pear', undefined], +// ] +// +// Into this: +// +// [ +// [123, 456, 789, 1000], +// ['orange', 'apple', 'banana', 'pear'], +// [null, true, false, undefined], +// ] +// +// And back again, if you call it again on its results. +export function transposeArrays(arrays) { + if (empty(arrays)) { + return []; + } + + const length = arrays[0].length; + const results = new Array(length).fill(null).map(() => []); + + for (const array of arrays) { + for (let i = 0; i < length; i++) { + results[i].push(array[i]); } -}; + } + + return results; +} -export const mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn)); +export const mapInPlace = (array, fn) => + array.splice(0, array.length, ...array.map(fn)); -export const filterEmptyLines = string => string.split('\n').filter(line => line.trim()).join('\n'); +export const unique = (arr) => Array.from(new Set(arr)); -export const unique = arr => Array.from(new Set(arr)); +export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) => + arr1.length === arr2.length && + (checkOrder + ? arr1.every((x, i) => arr2[i] === x) + : arr1.every((x) => arr2.includes(x))); // Stolen from jq! Which pro8a8ly stole the concept from other places. Nice. -export const withEntries = (obj, fn) => Object.fromEntries(fn(Object.entries(obj))); +export const withEntries = (obj, fn) => + Object.fromEntries(fn(Object.entries(obj))); + +export function setIntersection(set1, set2) { + const intersection = new Set(); + for (const item of set1) { + if (set2.has(item)) { + intersection.add(item); + } + } + return intersection; +} -// Nothin' more to it than what it says. Runs a function in-place. Provides an -// altern8tive syntax to the usual IIFEs (e.g. (() => {})()) when you want to -// open a scope and run some statements while inside an existing expression. -export const call = fn => fn(); +export function filterProperties(object, properties, { + preserveOriginalOrder = false, +} = {}) { + if (typeof object !== 'object' || object === null) { + throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`); + } -export function queue(array, max = 50) { - if (max === 0) { - return array.map(fn => fn()); + if (!Array.isArray(properties)) { + throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`); + } + + const filteredObject = {}; + + if (preserveOriginalOrder) { + for (const property of Object.keys(object)) { + if (properties.includes(property)) { + filteredObject[property] = object[property]; + } } + } else { + for (const property of properties) { + if (Object.hasOwn(object, property)) { + filteredObject[property] = object[property]; + } + } + } - const begin = []; - let current = 0; - const ret = array.map(fn => new Promise((resolve, reject) => { + return filteredObject; +} + +export function queue(array, max = 50) { + if (max === 0) { + return array.map((fn) => fn()); + } + + const begin = []; + let current = 0; + const ret = array.map( + (fn) => + new Promise((resolve, reject) => { begin.push(() => { - current++; - Promise.resolve(fn()).then(value => { - current--; - if (current < max && begin.length) { - begin.shift()(); - } - resolve(value); - }, reject); + current++; + Promise.resolve(fn()).then((value) => { + current--; + if (current < max && begin.length) { + begin.shift()(); + } + resolve(value); + }, reject); }); - })); + }) + ); - for (let i = 0; i < max && begin.length; i++) { - begin.shift()(); - } + for (let i = 0; i < max && begin.length; i++) { + begin.shift()(); + } - return ret; + return ret; } export function delay(ms) { - return new Promise(res => setTimeout(res, ms)); + return new Promise((res) => setTimeout(res, ms)); } // Stolen from here: https://stackoverflow.com/a/3561711 // // There's a proposal for a native JS function like this, 8ut it's not even -// past stage 1 yet: https://github.com/tc39/proposal-regex-escaping +// past stage ~~1~~ 2 yet: https://github.com/tc39/proposal-regex-escaping export function escapeRegex(string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); } -export function bindOpts(fn, bind) { - const bindIndex = bind[bindOpts.bindIndex] ?? 1; +export function splitKeys(key) { + return key.split(/(?<=(?<!\\)(?:\\\\)*)\./); +} - return (...args) => { - const opts = args[bindIndex] ?? {}; - return fn(...args.slice(0, bindIndex), {...bind, ...opts}); - }; +// Follows a key path like 'foo.bar.baz' to get an item nested deeply inside +// an object. +export function getNestedProp(obj, key) { + const recursive = (o, k) => + (k.length === 1 + ? o[k[0]] + : recursive(o[k[0]], k.slice(1))); + + return recursive(obj, splitKeys(key)); } -bindOpts.bindIndex = Symbol(); +// Gets the "look" of some arbitrary value. It's like typeof, but smarter. +// Don't use this for actually validating types - it's only suitable for +// inclusion in error messages. +export function typeAppearance(value) { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (Array.isArray(value)) return 'array'; + return typeof value; +} + +// Limits a string to the desired length, filling in an ellipsis at the end +// if it cuts any text off. +export function cut(text, length = 40) { + if (text.length >= length) { + const index = Math.max(1, length - 3); + return text.slice(0, index) + '...'; + } else { + return text; + } +} + +// Limits a string to the desired length, filling in an ellipsis at the start +// if it cuts any text off. +export function cutStart(text, length = 40) { + if (text.length >= length) { + const index = Math.min(text.length - 1, text.length - length + 3); + return '...' + text.slice(index); + } else { + return text; + } +} -// Utility function for providing useful interfaces to the JS AggregateError -// class. +// Annotates {index, length} results from another iterator with contextual +// details, including: +// +// * its line and column numbers; +// * if `formatWhere` is true (the default), a pretty-formatted, +// human-readable indication of the match's placement in the string; +// * if `getContainingLine` is true, the entire line (or multiple lines) +// of text containing the match. // -// Generally, this works by returning a set of interfaces which operate on -// functions: wrap() takes a function and returns a new function which passes -// its arguments through and appends any resulting error to the internal error -// list; call() simplifies this process by wrapping the provided function and -// then calling it immediately. Once the process for which errors should be -// aggregated is complete, close() constructs and throws an AggregateError -// object containing all caught errors (or doesn't throw anything if there were -// no errors). -export function openAggregate({ - // Constructor to use, defaulting to the builtin AggregateError class. - // Anything passed here should probably extend from that! May be used for - // letting callers programatically distinguish between multiple aggregate - // errors. - // - // This should be provided using the aggregateThrows utility function. - [openAggregate.errorClassSymbol]: errorClass = AggregateError, - - // Optional human-readable message to describe the aggregate error, if - // constructed. - message = '', - - // Value to return when a provided function throws an error. If this is a - // function, it will be called with the arguments given to the function. - // (This is primarily useful when wrapping a function and then providing it - // to another utility, e.g. array.map().) - returnOnFail = null +export function* iterateMultiline(content, iterator, { + formatWhere = true, + getContainingLine = false, } = {}) { - const errors = []; - - const aggregate = {}; - - aggregate.wrap = fn => (...args) => { - try { - return fn(...args); - } catch (error) { - errors.push(error); - return (typeof returnOnFail === 'function' - ? returnOnFail(...args) - : returnOnFail); - } - }; + const lineRegexp = /\n/g; + const isMultiline = content.includes('\n'); + + let lineNumber = 0; + let startOfLine = 0; + let previousIndex = 0; + + const countLineBreaks = (index, length) => { + const range = content.slice(index, index + length); + const lineBreaks = Array.from(range.matchAll(lineRegexp)); + if (!empty(lineBreaks)) { + lineNumber += lineBreaks.length; + startOfLine = index + lineBreaks.at(-1).index + 1; + } + }; - aggregate.call = (fn, ...args) => { - return aggregate.wrap(fn)(...args); - }; + for (const result of iterator) { + const {index, length} = result; - aggregate.nest = (...args) => { - return aggregate.call(() => withAggregate(...args)); - }; + countLineBreaks(previousIndex, index - previousIndex); - aggregate.map = (...args) => { - const parent = aggregate; - const { result, aggregate: child } = mapAggregate(...args); - parent.call(child.close); - return result; - }; + const matchStartOfLine = startOfLine; - aggregate.filter = (...args) => { - const parent = aggregate; - const { result, aggregate: child } = filterAggregate(...args); - parent.call(child.close); - return result; - }; + previousIndex = index + length; - aggregate.throws = aggregateThrows; + const columnNumber = index - startOfLine; - aggregate.close = () => { - if (errors.length) { - throw Reflect.construct(errorClass, [errors, message]); - } - }; + let where = null; + if (formatWhere) { + where = + colors.yellow( + (isMultiline + ? `line: ${lineNumber + 1}, col: ${columnNumber + 1}` + : `pos: ${index + 1}`)); + } + + countLineBreaks(index, length); + + let containingLine = null; + if (getContainingLine) { + const nextLineResult = + content + .slice(previousIndex) + .matchAll(lineRegexp) + .next(); - return aggregate; + const nextStartOfLine = + (nextLineResult.done + ? content.length + : previousIndex + nextLineResult.value.index); + + containingLine = + content.slice(matchStartOfLine, nextStartOfLine); + } + + yield { + ...result, + lineNumber, + columnNumber, + where, + containingLine, + }; + } } -openAggregate.errorClassSymbol = Symbol('error class'); +// Iterates over regular expression matches within a single- or multiline +// string, yielding each match as well as contextual details; this accepts +// the same options (and provides the same context) as iterateMultiline. +export function* matchMultiline(content, matchRegexp, options) { + const matchAllIterator = + content.matchAll(matchRegexp); + + const cleanMatchAllIterator = + (function*() { + for (const match of matchAllIterator) { + yield { + index: match.index, + length: match[0].length, + match, + }; + } + })(); + + const multilineIterator = + iterateMultiline(content, cleanMatchAllIterator, options); + + yield* multilineIterator; +} -// Utility function for providing {errorClass} parameter to aggregate functions. -export function aggregateThrows(errorClass) { - return {[openAggregate.errorClassSymbol]: errorClass}; +// Binds default values for arguments in a {key: value} type function argument +// (typically the second argument, but may be overridden by providing a +// [bindOpts.bindIndex] argument). Typically useful for preparing a function for +// reuse within one or multiple other contexts, which may not be aware of +// required or relevant values provided in the initial context. +// +// This function also passes the identity of `this` through (the returned value +// is not an arrow function), though note it's not a true bound function either +// (since Function.prototype.bind only supports positional arguments, not +// "options" specified via key/value). +// +export function bindOpts(fn, bind) { + const bindIndex = bind[bindOpts.bindIndex] ?? 1; + + const bound = function (...args) { + const opts = args[bindIndex] ?? {}; + return Reflect.apply(fn, this, [ + ...args.slice(0, bindIndex), + {...bind, ...opts} + ]); + }; + + annotateFunction(bound, { + name: fn, + trait: 'options-bound', + }); + + for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(fn))) { + if (key === 'length') continue; + if (key === 'name') continue; + if (key === 'arguments') continue; + if (key === 'caller') continue; + if (key === 'prototype') continue; + Object.defineProperty(bound, key, descriptor); + } + + return bound; } -// Performs an ordinary array map with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. +bindOpts.bindIndex = Symbol(); + +// Sorts multiple arrays by an arbitrary function (which is the last argument). +// Paired values from each array are provided to the callback sequentially: // -// Optionally, override returnOnFail to disable filtering and map errored inputs -// to a particular output. +// (a_fromFirstArray, b_fromFirstArray, +// a_fromSecondArray, b_fromSecondArray, +// a_fromThirdArray, b_fromThirdArray) => +// relative positioning (negative, positive, or zero) // -// Note the aggregate property is the result of openAggregate(), still unclosed; -// use aggregate.close() to throw the error. (This aggregate may be passed to a -// parent aggregate: `parent.call(aggregate.close)`!) -export function mapAggregate(array, fn, aggregateOpts) { - const failureSymbol = Symbol(); +// Like native single-array sort, this is a mutating function. +export function sortMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + const length = arrays[0].length; + const symbols = new Array(length).fill(null).map(() => Symbol()); + const indexes = Object.fromEntries(symbols.map((symbol, index) => [symbol, index])); + + symbols.sort((a, b) => { + const indexA = indexes[a]; + const indexB = indexes[b]; + + const args = []; + for (let i = 0; i < arrays.length; i++) { + args.push(arrays[i][indexA]); + args.push(arrays[i][indexB]); + } - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts - }); + return fn(...args); + }); - const result = array.map(aggregate.wrap(fn)) - .filter(value => value !== failureSymbol); + for (const array of arrays) { + // Note: We're mutating this array pulling values from itself, but only all + // at once after all those values have been pulled. + array.splice(0, array.length, ...symbols.map(symbol => array[indexes[symbol]])); + } - return {result, aggregate}; + return arrays; } -// Performs an ordinary array filter with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. +// Filters multiple arrays by an arbitrary function (which is the last argument). +// Values from each array are provided to the callback sequentially: // -// Optionally, override returnOnFail to disable filtering errors and map errored -// inputs to a particular output. +// (value_fromFirstArray, +// value_fromSecondArray, +// value_fromThirdArray, +// index, +// [firstArray, secondArray, thirdArray]) => +// true or false // -// As with mapAggregate, the returned aggregate property is not yet closed. -export function filterAggregate(array, fn, aggregateOpts) { - const failureSymbol = Symbol(); - - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts - }); - - const result = array.map(aggregate.wrap((x, ...rest) => ({ - input: x, - output: fn(x, ...rest) - }))) - .filter(value => { - // Filter out results which match the failureSymbol, i.e. errored - // inputs. - if (value === failureSymbol) return false; - - // Always keep results which match the overridden returnOnFail - // value, if provided. - if (value === aggregateOpts.returnOnFail) return true; - - // Otherwise, filter according to the returned value of the wrapped - // function. - return value.output; - }) - .map(value => { - // Then turn the results back into their corresponding input, or, if - // provided, the overridden returnOnFail value. - return (value === aggregateOpts.returnOnFail - ? value - : value.input); - }); +// Please be aware that this is a mutating function, unlike native single-array +// filter. The mutated arrays are returned. Also attached under `.removed` are +// corresponding arrays of items filtered out. +export function filterMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + const removed = new Array(arrays.length).fill(null).map(() => []); + + for (let i = arrays[0].length - 1; i >= 0; i--) { + const args = arrays.map(array => array[i]); + args.push(i, arrays); + + if (!fn(...args)) { + for (let j = 0; j < arrays.length; j++) { + const item = arrays[j][i]; + arrays[j].splice(i, 1); + removed[j].unshift(item); + } + } + } + + Object.assign(arrays, {removed}); + return arrays; +} + +// Corresponding filter function for sortByCount. By default, items whose +// corresponding count is zero will be removed. +export function filterByCount(data, counts, { + min = 1, + max = Infinity, +} = {}) { + filterMultipleArrays(data, counts, (data, count) => + count >= min && count <= max); +} + +// Reduces multiple arrays with an arbitrary function (which is the last +// argument). Note that this reduces into multiple accumulators, one for +// each input array, not just a single value. That's reflected in both the +// callback parameters: +// +// (accumulator1, +// accumulator2, +// value_fromFirstArray, +// value_fromSecondArray, +// index, +// [firstArray, secondArray]) => +// [newAccumulator1, newAccumulator2] +// +// As well as the final return value of reduceMultipleArrays: +// +// [finalAccumulator1, finalAccumulator2] +// +// This is not a mutating function. +export function reduceMultipleArrays(...args) { + const [arrays, fn, initialAccumulators] = + (typeof args.at(-1) === 'function' + ? [args.slice(0, -1), args.at(-1), null] + : [args.slice(0, -2), args.at(-2), args.at(-1)]); + + if (empty(arrays[0])) { + throw new TypeError(`Reduce of empty arrays with no initial value`); + } + + let [accumulators, i] = + (initialAccumulators + ? [initialAccumulators, 0] + : [arrays.map(array => array[0]), 1]); + + for (; i < arrays[0].length; i++) { + const args = [...accumulators, ...arrays.map(array => array[i])]; + args.push(i, arrays); + accumulators = fn(...args); + } + + return accumulators; +} + +export function chunkByConditions(array, conditions) { + if (empty(array)) { + return []; + } + + if (empty(conditions)) { + return [array]; + } + + const out = []; + let cur = [array[0]]; + for (let i = 1; i < array.length; i++) { + const item = array[i]; + const prev = array[i - 1]; + let chunk = false; + for (const condition of conditions) { + if (condition(item, prev)) { + chunk = true; + break; + } + } + if (chunk) { + out.push(cur); + cur = [item]; + } else { + cur.push(item); + } + } + out.push(cur); + return out; +} - return {result, aggregate}; +export function chunkByProperties(array, properties) { + return chunkByConditions( + array, + properties.map((p) => (a, b) => { + if (a[p] instanceof Date && b[p] instanceof Date) return +a[p] !== +b[p]; + + if (a[p] !== b[p]) return true; + + // Not sure if this line is still necessary with the specific check for + // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....? + if (a[p] != b[p]) return true; + + return false; + }) + ).map((chunk) => ({ + ...Object.fromEntries(properties.map((p) => [p, chunk[0][p]])), + chunk, + })); } -// Totally sugar function for opening an aggregate, running the provided -// function with it, then closing the function and returning the result (if -// there's no throw). -export function withAggregate(aggregateOpts, fn) { - if (typeof aggregateOpts === 'function') { - fn = aggregateOpts; - aggregateOpts = {}; +export function chunkMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + if (arrays[0].length === 0) { + return []; + } + + const newChunk = index => arrays.map(array => [array[index]]); + const results = [newChunk(0)]; + + for (let i = 1; i < arrays[0].length; i++) { + const current = results.at(-1); + + const args = []; + for (let j = 0; j < arrays.length; j++) { + const item = arrays[j][i]; + const previous = current[j].at(-1); + args.push(item, previous); } - const aggregate = openAggregate(aggregateOpts); - const result = fn(aggregate); - aggregate.close(); - return result; + if (fn(...args)) { + results.push(newChunk(i)); + continue; + } + + for (let j = 0; j < arrays.length; j++) { + current[j].push(arrays[j][i]); + } + } + + return results; } -export function showAggregate(topError) { - const recursive = error => { - const header = `[${error.constructor.name || 'unnamed'}] ${error.message || '(no message)'}`; - if (error instanceof AggregateError) { - return header + '\n' + (error.errors - .map(recursive) - .flatMap(str => str.split('\n')) - .map(line => ` | ` + line) - .join('\n')); - } else { - return header; - } - }; +// Delicious function annotations, such as: +// +// (*bound) soWeAreBackInTheMine +// (data *unfulfilled) generateShrekTwo +// +export function annotateFunction(fn, { + name: nameOrFunction = null, + description: newDescription, + trait: newTrait, +}) { + let name; + + if (typeof nameOrFunction === 'function') { + name = nameOrFunction.name; + } else if (typeof nameOrFunction === 'string') { + name = nameOrFunction; + } + + name ??= fn.name ?? 'anonymous'; + + const match = name.match(/^ *(?<prefix>.*?) *\((?<description>.*)( #(?<trait>.*))?\) *(?<suffix>.*) *$/); + + let prefix, suffix, description, trait; + if (match) { + ({prefix, suffix, description, trait} = match.groups); + } + + prefix ??= ''; + suffix ??= name; + description ??= ''; + trait ??= ''; + + if (newDescription) { + if (description) { + description += '; ' + newDescription; + } else { + description = newDescription; + } + } + + if (newTrait) { + if (trait) { + trait += ' #' + newTrait; + } else { + trait = '#' + newTrait; + } + } + + let parenthesesPart; + + if (description && trait) { + parenthesesPart = `${description} ${trait}`; + } else if (description || trait) { + parenthesesPart = description || trait; + } else { + parenthesesPart = ''; + } + + let finalName; + + if (prefix && parenthesesPart) { + finalName = `${prefix} (${parenthesesPart}) ${suffix}`; + } else if (parenthesesPart) { + finalName = `(${parenthesesPart}) ${suffix}`; + } else { + finalName = suffix; + } - console.log(recursive(topError)); + Object.defineProperty(fn, 'name', {value: finalName}); } |