UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

452 lines (439 loc) 15.9 kB
/** * @template T * @extends Array<T> */ export default class Ayay extends Array { /** * @template T * @param {Iterable<T> | ArrayLike<T>} iterable * @returns {Ayay<T>} */ static from(iterable) { return super.from(iterable) } /** * @template T * @param {T[]} items * @returns {Ayay<T>} */ static of(...items) { return super.of(...items) } /** * Creates a new array with one item removed. * @param {number} index index to remove * @returns {Ayay<T>} */ arrayByRemovingItemAtIndex(index) { return this.slice(0, index).concat(this.slice(index + 1)); } /** * Creates a new array with one item inserted. * @param item The new item * @param {number} atIndex The insertion index * @returns {Ayay<T>} */ arrayByInsertingItem(item, atIndex) { return this.slice(0, atIndex).concat(item, this.slice(atIndex)); } /** * Returns a new array with one item moved from one position to another. * @param {number} fromIndex The index to move from * @param {*} toIndex The destination index * @returns {Ayay} */ arrayByMoving(fromIndex, toIndex) { const movedItem = this[fromIndex]; return this .arrayByRemovingItemAtIndex(fromIndex) .arrayByInsertingItem(movedItem, toIndex - (fromIndex < toIndex)); } /** * Returns a new array, which is in reverse order. Unlike Array.prototype.reverse(), * this method does not mutate the original array. * @returns {Ayay} */ reversedArray() { return this.map((item, index, array) => array.at(-index - 1)); } /** * Performs a shallow compare with another array-like object. * If lengths are equal and every item is strictly equal to the item in the argument array, true is returned. * @param {any[]} array item to compare with */ isShallowIdenticalWith(array) { return array instanceof Array && this.length === array.length && this.every((item, index) => item === array[index]); } /** * Returns a new array by replacing the item at the specified position. * @param {number} index The index of the item to replace * @param {any} replacement The new item * @returns {Ayay} */ arrayByReplacingItemAtIndex(index, replacement) { return this.slice(0, index).concat(replacement, this.slice(index + 1)); } /** * Creates a new array by swapping items at given positions. * @param {number} index the first index * @param {number} withIndex the other index * @returns {Ayay} */ arrayBySwappingItems(index, withIndex) { return this .arrayByReplacingItemAtIndex(index, this[withIndex]) .arrayByReplacingItemAtIndex(withIndex, this[index]); } /** * Creates a new array sorted using the comparator function. * @param {(item1: any, item2: any) => number} comparator Called with arguments `item1` and `item2`, return negative value to sort `item1` before `item2`. * @returns {Ayay} */ arraySortedBy(comparator) { const sortedArray = new Ayay(); this.forEach(item => { let startIndex = 0, endIndex = sortedArray.length; while (startIndex !== endIndex) { const middleIndex = Math.floor((startIndex + endIndex) / 2); if (comparator(item, sortedArray[middleIndex]) > 0) { startIndex = middleIndex + 1; } else { endIndex = middleIndex; } } sortedArray.splice(startIndex, 0, item); }); return sortedArray; } /** * Calculates the sum of all items in the array. * @param {(item: any) => number } [mapper] Optional function to transform item before adding to sum. * @param {object} [thisArg] An object to which the `this` keyword can refer in the mapper function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns {number} */ sumItems(mapper = item => item, thisArg) { return this.reduce((total, item) => total + mapper.call(thisArg, item), 0); } /** * Calculates the average of the items in the array. * @param {(item: any) => number} [mapper] Optional function called on each item to transform it to a number. * @param {object} [thisArg] An object to which the `this` keyword can refer in the mapper function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns {number} */ average(mapper = item => item, thisArg) { return this.sumItems(mapper, thisArg) / this.length; } /** * Sum items as long as the `condition` is met. * @param {(value: any, index: number) => boolean} condition Function to determine if iteration should continue. * @param {(item: any, index: number, array: Ayay) => number} [mapper] Optional function to transform item before adding to sum. * @param {object} [thisArg] An object to which the `this` keyword can refer in the callbackfn function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns */ sumItemsWhile(condition, mapper = item => item, thisArg) { return this.reduceWhile((total, item) => total + mapper.call(thisArg, item), 0, condition, thisArg); } /** * The last element in the array. */ get last() { return this.at(-1) } /** * The index of the last element in the array. */ get lastIndex() { return this.length - 1; } /** * The first element in the array. */ get first() { return this[0]; } /** * a getter which returns the same as the `toVanilla()` function. */ get vanilla() { return this.toVanilla() } /** * Checks whether the array is empty or not. */ isEmpty() { return this.length === 0; } /** * For an array of functions, calling `.pipe` will return a function that will pass a value through all the array's functions. * Example: `Ayay.of(n => n * 2, n => n * 3).pipe(1) // 6` * @type {(any) => any} */ get pipe() { return this.reduce.bind(this, (val, func) => func(val)); } /** * Returns a new array with ordered indices of all items (`[1, 2, 3,..., length - 1]`). * @returns {Ayay} */ indices() { return this.map((item, index) => index); } /** * Returns a new array with all `null` and `undefined` elements removed. * @param {(item: any, index: number, array: Ayay) => any} [mapper] Optional transform function called for each element. * @param {object} [thisArg] An object to which the `this` keyword can refer in the callbackfn function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns {Ayay} */ compactMap(mapper, thisArg) { return (mapper ? this.map(mapper, thisArg) : this) .filter(item => item != null); } /** * Filter the array using an asynchronous tester function. * @param {(item: any, index: number) => Promise<boolean>} tester Async function called for each item in the array. * @returns {Promise<Ayay>} */ async asyncFilter(tester) { return Ayay.of(await Promise.all(this.map(async (item, index) => await tester(item, index) ? item : null ))).compactMap() } /** * Returns a new Array containing chunks of the original array of the given size. * @param {number} chunkSize The size of the chunks * @returns {Ayay} */ chunksOf(chunkSize) { return this.isEmpty() ? this : Ayay.of(this.slice(0, chunkSize)).concat(this.slice(chunkSize).chunksOf(chunkSize)); } /** * Similar to ordinary `forEach`, but before each `iterator` call, * the `condition` function will be called. If that function returns false, iteration will stop. * @param {(value: any, index: number, array: Ayay) => void} iterator Function called for each item. * @param {(value: any, index: number) => boolean} condition Function to determine if iteration should continue. * @param {object} [thisArg] An object to which the `this` keyword can refer in the callbackfn function. If `thisArg` is omitted, undefined is used as the `this` value. */ iterateWhile(iterator, condition, thisArg) { for (let index = 0; index < this.length && condition.call(thisArg, this[index], index); index += 1) { iterator.call(thisArg, this[index], index, this); } } /** * Similar to ordinary `reduce`, but before each `reducer` call, * the condition function will be called. If that function returns false, iteration will stop, and the function will return the accumulated value. * @param {(value: any, index: number, array: Ayay) => void} reducer * @param {any} initialValue The initial value of the iteration * @param {(value: any, index: number) => boolean} condition Function to determine if iteration should continue. * @param {object} [thisArg] An object to which the `this` keyword can refer in the callbackfn function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns {any} */ reduceWhile(reducer, initialValue, condition, thisArg) { let returnValue = initialValue; this.iterateWhile( (element, index) => returnValue = reducer.call(thisArg, returnValue, element, index, this), (element, index) => condition.call(thisArg, returnValue, element, index), ); return returnValue; } /** * Transform the array using the mapper function. Before each call to `mapper`, `condition` is called. If `condition` returns false, the function returns the mapped collection, which can be shorter than the original array. * @param {(item: any, index: number, array: Ayay) => any} mapper Function called for each item in array * @param {(item: any, index: number, array: Ayay) => boolean} condition Function * @param {object} [thisArg] An object to which the `this` keyword can refer in the callbackfn function. If `thisArg` is omitted, undefined is used as the `this` value. * @returns {Ayay} */ mapWhile(mapper, condition, thisArg) { return this.reduceWhile( (mappedArray, element, index) => mappedArray.concat(mapper.call(thisArg, element, index, this)), Ayay.of(), (mappedArray, element, index) => condition.call(thisArg, element, index, this) ); } /** * Returns array with unique items. Pass a function to select how to check uniqueness. * @param mapper * @returns {Ayay} Ayay with unique items */ uniqueItems(mapper = item => item) { const uniqueItems = new Set(); return this.filter((...args) => { const unique = mapper(...args); if (!uniqueItems.has(unique)) { uniqueItems.add(unique) return true; } }); } /** * Returns a new array containing all but the last item of the original array. * @returns {Ayay} */ arrayByDroppingLastItem() { return this.arrayByRemovingItemAtIndex(this.lastIndex); } /** * Joins elements putting `lastSeperator` between two last elements. * @param {string} [lastSeparator='and'] The last separator */ prettyJoin(lastSeparator = 'and') { return [] .concat(this.arrayByDroppingLastItem().join(', ') || []) .concat(this.slice(-1)) .join(` ${lastSeparator} `); } /** * Constructs a new Ayay by splitting a string on the format returned by `prettyJoin`, for example `'cat and dog'`. * @param {string} string Input string * @param {string} [lastSeparator='and'] The word between the two last items. Default 'and'. * @returns {Ayay} */ static fromPrettyJoined(string, lastSeparator = 'and') { return Ayay.from(string.split(` ${lastSeparator} `) .flatMap(part => part.split(', '))) } /** * Creates an array instance with given size and content. * @param {number} size The length of the new array * @param {any} template The content for each item. * If it is an object, each item in the array will be a shallow copy. * If it is a function, it will be called for each item with the index. * @returns {Ayay} */ static seedWith(size, template = 'ayay') { const newArray = new Ayay(size); switch (typeof template) { case 'function': return newArray.fill(0).map((_, index) => template(index)); case 'object': return newArray.fill(0).map(() => template instanceof Array ? [...template] : { ...template }); default: return newArray.fill(template); } } /** * Returns a shallow copy as an ordinary Array instance. */ toVanilla() { return [...this]; } // Random utils /** * Returns a shallow copy of the array, with elements shuffled. * @returns {Ayay} */ shuffledArray() { return this.sampleOfSize(this.length); } /** * Returns a random element from the array. */ randomItem() { return this[this.randomIndex()]; } /** * Returns a random index of the array. * @returns {number} */ randomIndex() { return Math.floor(Math.random() * this.length); } /** * Returns a new array with some of the items of the original array, in random order. * @param {number} size The size of the sample * @returns {Ayay} */ sampleOfSize(size) { if (size === 0 || this.length === 0) { return Ayay.of(); } const pickedIndex = this.randomIndex(); return Ayay.of(this[pickedIndex], ...this.arrayByRemovingItemAtIndex(pickedIndex).sampleOfSize(size - 1) ); } /** * Returns an array holding all possible permutations of the original array. * @returns {Ayay} */ permutations() { return this.length <= 1 ? [this] : this.map( (firstElement, index) => this.arrayByRemovingItemAtIndex(index) .permutations() .map(permutation => [firstElement, ...permutation]) ).flat() } // Object utils /** * Resolves a value from an object using the elements in the array as a key path. * Example `Ayay.of('a', 'b', 'c').useAsKeyPathIn({a: {b: {c: 'yes'}}}) // 'yes'` * @param {any} object The object to get from * @param {any} [defaultValue] Optional value to return if nothing is found at given keypath */ useAsKeyPathIn(object, defaultValue) { const value = this.reduceWhile( (currentTarget, key) => currentTarget && currentTarget[key], object, target => target ); return value === undefined ? defaultValue : value; } /** * Use the array elements as keys to pick in a given object. * Returns a new object with given keys picked from `object`, and the remaining keys omitted. * @param {object} object The object to pick from */ pickInObject(object) { return this.reduce( (pickedObject, key) => Object.assign(pickedObject, key in object && { [key]: object[key] }), {} ); } /** * Use the array elements as keys to omit in a given object. * Returns a new object with given keys omitted from `object`. * @param {object} object */ omitInObject(object) { return this.reduce( (omittedObject, key) => { const { [key]: omitted, ...keptItems } = omittedObject; return keptItems; }, object); } /** * Converts the array into an object using the provider mappers. Default behaviour converts values to keys, and indices to values: * `Ayay.of('a', 'b', 'c').transpose() // {a: 1, b: 2, c: 3}` * @param {{keyMapper?: (value: any, index: number) => string, valueMapper?: (value: any, index: number) => any }} [param0] */ transpose({ keyMapper = value => value, valueMapper = (value, index) => index } = {}) { return this.reduce( (object, item, index) => Object.assign(object, { [keyMapper(item, index, this)]: valueMapper(item, index, this) }), {} ); } /** * Group items by either a value of a property, or by a function called for each item. * Returns an object with group names as keys, and group members as values. * Example: * * `Ayay.of({name: 'carrot', type: 'integer'}, {name: 'tomato', type}, {name: 'banana', type: 'fruit'} * ).groupBy('type', ({name}) => name) // { vegetable: ['carrot', 'tomato'], fruit: ['banana'] }` * @param {string|(value: any) => string} groupDeterminant String or function called with item to determine group for item. * @param {(value: any) => any} [valueMapper] Optional transform function for the value. Defaults to identity function. * @returns {{[group: string]: Ayay}} */ groupBy(groupDeterminant, valueMapper = value => value) { return this.reduce((groups, item) => { const group = typeof groupDeterminant == 'string' ? item[groupDeterminant] : groupDeterminant(item) if (group) { (groups[group] ??= []).push(valueMapper(item)) } return groups }, {}) } }