UNPKG

@ugo-code/streamline.js

Version:

A utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript

229 lines (228 loc) 9.68 kB
/** * An extended Array class providing convenient getters and powerful utility methods. * It's fully generic, type-safe, and designed for seamless method chaining. * * @template T The type of elements in the array. Defaults to `unknown`. */ export class ArraySL extends Array { /** * Creates a new ArraySL instance. This constructor is compatible with the * standard Array constructor signatures. */ constructor(item = []) { // This logic handles the different ways an Array can be constructed. // 1. new ArraySL(5) -> called by .map() etc., creates an empty array of length 5. // 2. new ArraySL(item1, item2) -> creates an array with the given items. if (typeof item === "number") { super(item); } else { super(...item); } // This is the crucial fix for ES5 targets. // It manually sets the prototype of the new instance to ArraySL.prototype. Object.setPrototypeOf(this, new.target.prototype); } /** * Ensures that methods inherited from Array (like .map, .filter, .slice) * return an instance of ArraySL instead of a plain Array. This makes * method chaining seamless. */ static get [Symbol.species]() { return this; } /** * Returns the first element of the array. * @returns {T | undefined} The first element, or `undefined` if the array is empty. */ get first() { return this[0]; } /** * Returns the last element of the array. * @returns {T | undefined} The last element, or `undefined` if the array is empty. */ get last() { return this[this.length - 1]; } /** * Returns a random element from the array. * @returns {T | undefined} A random element, or `undefined` if the array is empty. */ get random() { if (this.length === 0) { return undefined; } return this[Math.floor(Math.random() * this.length)]; } /** * Returns the middle item or items of the array based on their index. * * This method finds the element(s) at the center of the array without any sorting. * - If the array has an odd number of elements, it returns the single middle item. * - If the array has an even number of elements, it returns the two middle items. * * This is distinct from a median calculation, which would require the array to be sorted by value first. * * @returns {ArraySL<T>} A new ArraySL containing the middle item(s). Returns an empty array if the source array is empty. * @example * // For an array with an odd length * const oddList = new ArraySL(['a', 'b', 'c', 'd', 'e']); * console.log(oddList.middle()); // Returns ArraySL['c'] * * // For an array with an even length * const evenList = new ArraySL([10, 20, 30, 40]); * console.log(evenList.middle()); // Returns ArraySL[20, 30] * * // For an empty array * const emptyList = new ArraySL([]); * console.log(emptyList.middle()); // Returns ArraySL[] */ middle() { if (this.length === 0) { return new ArraySL(); } const midIndex = Math.floor(this.length / 2); if (this.length % 2 !== 0) { // Odd length: return the single middle item by index return new ArraySL([this[midIndex]]); } else { // Even length: return the two middle items by index return new ArraySL([this[midIndex - 1], this[midIndex]]); } } /** * Returns a new ArraySL containing only the unique elements from the original array. * Uniqueness is based on the element itself or on a key generated by the provided accessor. * This method is highly performant (O(n)). * * @param options An optional configuration object. * - `accessor`: An optional function that takes an element and returns a value to be used * for the uniqueness check. If not provided, the element itself is used. Keys that are * objects or arrays are compared by their JSON string representation. * @returns {ArraySL<T>} A new ArraySL instance with duplicate elements removed. */ unique(options = {}) { const { accessor = (value) => value } = options; const seen = new Set(); return this.filter((value, index, array) => { const key = accessor(value, index, array); if (seen.has(key)) { return false; } seen.add(key); return true; }); } /** * Returns a new ArraySL containing duplicate elements from the original array. * This method is highly configurable and performant, operating in a single pass (O(n)). * * @param options An optional configuration object. * - `accessor`: An optional function that takes an element and returns a value to be used * for the uniqueness check. If not provided, the element itself is used. Keys that are * objects or arrays are compared by their JSON string representation. * - `mode`: Determines which duplicate items to include in the result. * - 'all' (default): Returns all instances of items that are duplicates. * - 'first': Returns only the first instance of each duplicated item. * - 'subsequent': Returns only the duplicate instances that appear after the first one. * @returns {ArraySL<T>} A new ArraySL instance containing the specified duplicate elements. */ duplicates(options = {}) { const { mode = "all", accessor = (value) => value } = options; const result = new ArraySL(); // The map tracks the state of each key using a dedicated object. const seen = new Map(); this.forEach((item, index, array) => { const key = accessor(item, index, array); const state = seen.get(key); if (state === undefined) { // First time seeing this key. Store the item with processed: false. seen.set(key, { item: item, processed: false }); } else { // This is a subsequent encounter (2nd, 3rd, etc.). if (!state.processed) { // This block runs only on the second encounter for a given key. if (mode === "first" || mode === "all") { // Add the original item we stored earlier. result.push(state.item); } // Mark as processed so this block doesn't run again for this key. state.processed = true; } // This part runs for all subsequent encounters (2nd, 3rd, 4th...). if (mode === "subsequent" || mode === "all") { result.push(item); } } }); return result; } /** * Finds the most frequently occurring item(s) in the array. * * This method identifies which items appear most often, based on the item itself or a * key extracted from the item. If multiple items share the same highest frequency, * all of them are returned. * * @param accessor An optional function to extract a comparable key from an item. If not provided, * the item itself is used for comparison. * @returns {ArraySL<T>} A new ArraySL containing the most frequent item(s). * Returns an empty array if there is no unique most frequent item (e.g., in `[1, 1, 2, 2]`). * @example * const users = new ArraySL([ * { name: 'Alice', city: 'Paris' }, * { name: 'Bob', city: 'Tokyo' }, * { name: 'Charlie', city: 'Paris' } * ]); * console.log(users.mostFrequent((u) => u.city)); // Returns ArraySL containing the 'Alice' and 'Charlie' objects * * const numbers = new ArraySL([1, 2, 2, 3, 3, 3]); * console.log(numbers.mostFrequent()); // Returns ArraySL[3, 3, 3] */ mostFrequent(accessor = (value) => value) { if (this.length === 0) { return new ArraySL(); } const frequencies = new Map(); let maxFreq = 0; this.forEach((value, index, array) => { const key = accessor(value, index, array); const newCount = (frequencies.get(key) || 0) + 1; frequencies.set(key, newCount); if (newCount > maxFreq) { maxFreq = newCount; } }); if (maxFreq <= 1 && this.length > 1) { return new ArraySL(); } const modeKeys = new Set(); for (const [key, freq] of frequencies.entries()) { if (freq === maxFreq) { modeKeys.add(key); } } if (modeKeys.size > 1 && (modeKeys.size * maxFreq) === this.length) { return new ArraySL(); } return this.filter((value, index, array) => modeKeys.has(accessor(value, index, array))); } filter(predicate, thisArg) { // The implementation is inherited from Array, so we just call super. return super.filter(predicate, thisArg); } // Overrides the default return type of `Array.prototype.map` to return `ArraySL<U>`. map(callbackfn, thisArg) { return super.map(callbackfn, thisArg); } // Overrides the default return type of `Array.prototype.slice` to return `ArraySL<T>`. slice(start, end) { return super.slice(start, end); } concat(...items) { return super.concat(...items); } }