Created
November 24, 2025 13:03
-
-
Save MagnusThor/c702f015b3ee38a6ab147dd8d9b65744 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A custom array class that provides additional query-like methods for filtering, mapping, and manipulating arrays. | |
| * @template T - The type of elements in the array. | |
| */ | |
| export class QueryableArray<T> extends Array<T> { | |
| /** | |
| * Skips the specified number of elements and returns the remaining elements. | |
| * @param count - The number of elements to skip. | |
| * @returns A new QueryableArray containing the remaining elements. | |
| */ | |
| skip(count: number): QueryableArray<T> { | |
| const result = QueryableArray.from(this.slice(count)); | |
| return result; | |
| } | |
| /** | |
| * Takes the specified number of elements from the start of the array. | |
| * @param count - The number of elements to take. | |
| * @returns A new QueryableArray containing the taken elements. | |
| */ | |
| take(count: number): QueryableArray<T> { | |
| const result = QueryableArray.from(this.slice(0, count)); | |
| return result; | |
| } | |
| /** | |
| * Filters the array based on a predicate function. | |
| * @param predicate - A function to test each element. | |
| * @returns A new QueryableArray containing the elements that satisfy the predicate. | |
| */ | |
| where(predicate: (item: T) => boolean): QueryableArray<T> { | |
| const result = QueryableArray.from(this.filter(predicate)); | |
| return result; | |
| } | |
| /** | |
| * Projects each element of the array into a new form. | |
| * @template U - The type of the projected elements. | |
| * @param selector - A function to transform each element. | |
| * @returns A new QueryableArray containing the transformed elements. | |
| */ | |
| select<U>(selector: (item: T) => U): QueryableArray<U> { | |
| const result = QueryableArray.from(this.map(selector)); | |
| return result; | |
| } | |
| /** | |
| * Returns the first element that satisfies the predicate or the first element if no predicate is provided. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The first matching element. | |
| * @throws An error if no matching element is found. | |
| */ | |
| first(predicate?: (item: T) => boolean): T { | |
| const item = predicate ? this.find(predicate) : this[0]; | |
| if (item === undefined) { | |
| throw new Error('Sequence contains no matching element'); | |
| } | |
| return item; | |
| } | |
| /** | |
| * Returns the first element that satisfies the predicate or undefined if no matching element is found. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The first matching element or undefined. | |
| */ | |
| firstOrDefault(predicate?: (item: T) => boolean): T | undefined { | |
| return predicate ? this.find(predicate) : this[0]; | |
| } | |
| /** | |
| * Returns the last element that satisfies the predicate or the last element if no predicate is provided. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The last matching element. | |
| * @throws An error if no matching element is found. | |
| */ | |
| last(predicate?: (item: T) => boolean): T { | |
| const items = predicate ? this.filter(predicate) : this; | |
| if (items.length === 0) { | |
| throw new Error('Sequence contains no matching element'); | |
| } | |
| return items[items.length - 1]; | |
| } | |
| /** | |
| * Returns the last element that satisfies the predicate or undefined if no matching element is found. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The last matching element or undefined. | |
| */ | |
| lastOrDefault(predicate?: (item: T) => boolean): T | undefined { | |
| const items = predicate ? this.filter(predicate) : this; | |
| return items.length > 0 ? items[items.length - 1] : undefined; | |
| } | |
| /** | |
| * Returns the only element that satisfies the predicate or the only element if no predicate is provided. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The single matching element. | |
| * @throws An error if there is not exactly one matching element. | |
| */ | |
| single(predicate?: (item: T) => boolean): T { | |
| const items = predicate ? this.filter(predicate) : this; | |
| if (items.length !== 1) { | |
| throw new Error('Sequence contains more than one matching element'); | |
| } | |
| return items[0]; | |
| } | |
| /** | |
| * Returns the only element that satisfies the predicate or undefined if no matching element is found. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The single matching element or undefined. | |
| */ | |
| singleOrDefault(predicate?: (item: T) => boolean): T | undefined { | |
| const items = predicate ? this.filter(predicate) : this; | |
| return items.length === 1 ? items[0] : undefined; | |
| } | |
| /** | |
| * Determines whether any elements satisfy the predicate or whether the array contains any elements if no predicate is provided. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns True if any elements satisfy the predicate or if the array contains any elements. | |
| */ | |
| any(predicate?: (item: T) => boolean): boolean { | |
| return predicate ? this.some(predicate) : this.length > 0; | |
| } | |
| /** | |
| * Determines whether all elements satisfy the predicate. | |
| * @param predicate - A function to test each element. | |
| * @returns True if all elements satisfy the predicate. | |
| */ | |
| all(predicate: (item: T) => boolean): boolean { | |
| return this.every(predicate); | |
| } | |
| /** | |
| * Counts the number of elements that satisfy the predicate or the total number of elements if no predicate is provided. | |
| * @param predicate - A function to test each element (optional). | |
| * @returns The count of matching elements. | |
| */ | |
| count(predicate?: (item: T) => boolean): number { | |
| return predicate ? this.filter(predicate).length : this.length; | |
| } | |
| /** | |
| * Sorts the elements in ascending order based on a key. | |
| * @template K - The type of the key. | |
| * @param keySelector - A function to extract the key for each element. | |
| * @returns A new QueryableArray containing the sorted elements. | |
| */ | |
| orderBy<K extends keyof T>(keySelector: (item: T) => T[K]): QueryableArray<T> { | |
| const result = new QueryableArray(...this.sort((a, b) => { | |
| if (keySelector(a) < keySelector(b)) return -1; | |
| if (keySelector(a) > keySelector(b)) return 1; | |
| return 0; | |
| })); | |
| return result; | |
| } | |
| /** | |
| * Sorts the elements in descending order based on a key. | |
| * @template K - The type of the key. | |
| * @param keySelector - A function to extract the key for each element. | |
| * @returns A new QueryableArray containing the sorted elements. | |
| */ | |
| orderByDescending<K extends keyof T>(keySelector: (item: T) => T[K]): QueryableArray<T> { | |
| const result = new QueryableArray(...this.sort((a, b) => { | |
| if (keySelector(a) > keySelector(b)) return -1; | |
| if (keySelector(a) < keySelector(b)) return 1; | |
| return 0; | |
| })); | |
| return result; | |
| } | |
| /** | |
| * Groups the elements of the array based on a key. | |
| * @template K - The type of the key. | |
| * @param keySelector - A function to extract the key for each element. | |
| * @returns A Map where the keys are the group keys and the values are QueryableArrays of grouped elements. | |
| */ | |
| groupBy<K>(keySelector: (item: T) => K): Map<K, QueryableArray<T>> { | |
| const map = new Map<K, QueryableArray<T>>(); | |
| this.forEach(item => { | |
| const key = keySelector(item); | |
| if (!map.has(key)) { | |
| map.set(key, new QueryableArray<T>()); | |
| } | |
| map.get(key)!.push(item); | |
| }); | |
| return map; | |
| } | |
| /** | |
| * Removes duplicate elements from the array. | |
| * @returns A new QueryableArray containing only distinct elements. | |
| */ | |
| distinct(): QueryableArray<T> { | |
| const set = new Set(this); | |
| return new QueryableArray(...set); | |
| } | |
| orderByAsc<K>(keySelector: (item: T) => K): QueryableArray<T> { | |
| const result = new QueryableArray(...this.sort((a, b) => { | |
| const keyA = keySelector(a); | |
| const keyB = keySelector(b); | |
| if (keyA < keyB) return -1; | |
| if (keyA > keyB) return 1; | |
| return 0; | |
| })); | |
| return result; | |
| } | |
| /** | |
| * Sorts the elements in descending order based on a key. | |
| * @template K - The type of the key. | |
| * @param keySelector - A function to extract the key for each element. | |
| * @returns A new QueryableArray containing the sorted elements. | |
| */ | |
| orderByDesc<K>(keySelector: (item: T) => K): QueryableArray<T> { | |
| const result = new QueryableArray(...this.sort((a, b) => { | |
| const keyA = keySelector(a); | |
| const keyB = keySelector(b); | |
| if (keyA > keyB) return -1; | |
| if (keyA < keyB) return 1; | |
| return 0; | |
| })); | |
| return result; | |
| } | |
| /** | |
| * Adds a single item to the array. | |
| * @param item - Item to add. | |
| * @returns This QueryableArray (chainable). | |
| */ | |
| add(item: T): this { | |
| this.push(item); | |
| return this; | |
| } | |
| /** | |
| * Adds multiple items to the array. | |
| * @param items - Collection of items to add. | |
| * @returns This QueryableArray (chainable). | |
| */ | |
| addRange(items: Iterable<T>): this { | |
| for (const item of items) { | |
| this.push(item); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Removes the first occurrence of an item from the array. | |
| * @param item - Item to remove. | |
| * @returns True if removed; false if not found. | |
| */ | |
| remove(item: T): boolean { | |
| const index = this.indexOf(item); | |
| if (index === -1) return false; | |
| this.splice(index, 1); | |
| return true; | |
| } | |
| /** | |
| * Removes the item at the specified index. | |
| * @param index - Index of the item to remove. | |
| * @returns True if removed; false if index invalid. | |
| */ | |
| removeAt(index: number): boolean { | |
| if (index < 0 || index >= this.length) return false; | |
| this.splice(index, 1); | |
| return true; | |
| } | |
| /** | |
| * Removes 'count' elements starting at 'start'. | |
| * @param start - Start index. | |
| * @param count - Number of items to remove. | |
| */ | |
| removeRange(start: number, count: number): void { | |
| this.splice(start, count); | |
| } | |
| /** | |
| * Returns a new array excluding values found in 'other'. | |
| * @param other - Items to exclude. | |
| */ | |
| except(other: Iterable<T>): QueryableArray<T> { | |
| const excludeSet = new Set(other); | |
| return new QueryableArray(...this.filter(x => !excludeSet.has(x))); | |
| } | |
| /** | |
| * Returns a new array containing only values also in 'other'. | |
| * @param other - Items to intersect with. | |
| */ | |
| intersect(other: Iterable<T>): QueryableArray<T> { | |
| const setB = new Set(other); | |
| return new QueryableArray(...this.filter(x => setB.has(x))); | |
| } | |
| /** | |
| * Returns a new array with unique items from both arrays. | |
| * @param other - Items to include. | |
| */ | |
| union(other: Iterable<T>): QueryableArray<T> { | |
| return new QueryableArray(...new Set([...this, ...other])); | |
| } | |
| /** | |
| * Flattens one level of nested arrays. | |
| */ | |
| flatten<U>(this: QueryableArray<U[] | U>): QueryableArray<U> { | |
| const flat = ([] as U[]).concat(...(this as unknown as U[][])); | |
| return new QueryableArray<U>(...flat); | |
| } | |
| /** | |
| * Sums all numeric values or values returned by the selector. | |
| */ | |
| sum(selector?: (item: T) => number): number { | |
| if (selector) return this.reduce((acc, x) => acc + selector(x), 0); | |
| return this.reduce((acc, x) => acc + (x as unknown as number), 0); | |
| } | |
| /** | |
| * Average of numeric values. | |
| */ | |
| average(selector?: (item: T) => number): number { | |
| return this.sum(selector) / this.length; | |
| } | |
| /** | |
| * Minimum element or minimum key value. | |
| */ | |
| min(selector?: (item: T) => number): number { | |
| if (selector) return Math.min(...this.map(selector)); | |
| return Math.min(...(this as unknown as number[])); | |
| } | |
| /** | |
| * Maximum element or maximum key value. | |
| */ | |
| max(selector?: (item: T) => number): number { | |
| if (selector) return Math.max(...this.map(selector)); | |
| return Math.max(...(this as unknown as number[])); | |
| } | |
| /** | |
| * Converts array into a dictionary/map. | |
| */ | |
| toDictionary<K, V>( | |
| keySelector: (item: T) => K, | |
| valueSelector: (item: T) => V | |
| ): Map<K, V> { | |
| const map = new Map<K, V>(); | |
| this.forEach(item => map.set(keySelector(item), valueSelector(item))); | |
| return map; | |
| } | |
| /** | |
| * Returns a specific page of items. | |
| * @param page - Page number (1-based). | |
| * @param size - Page size (items per page). | |
| */ | |
| paginate(page: number, size: number): QueryableArray<T> { | |
| const start = (page - 1) * size; | |
| const slice = this.slice(start, start + size); | |
| return QueryableArray.from(slice); | |
| } | |
| /** | |
| * Returns distinct items based on a selector. | |
| * Similar to C# LINQ DistinctBy. | |
| * @param selector - A function that selects the comparison key. | |
| */ | |
| uniqueBy<K>(selector: (item: T) => K): QueryableArray<T> { | |
| const seen = new Set<K>(); | |
| const result = new QueryableArray<T>(); | |
| for (const item of this) { | |
| const key = selector(item); | |
| if (!seen.has(key)) { | |
| seen.add(key); | |
| result.push(item); | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Sorts by multiple keys in priority order. | |
| * Example: | |
| * arr.sortByMultiple([ | |
| * x => x.lastName, | |
| * x => x.firstName, | |
| * x => x.age | |
| * ]) | |
| */ | |
| sortByMultiple(selectors: Array<(item: T) => any>): QueryableArray<T> { | |
| const result = new QueryableArray(...this); | |
| result.sort((a, b) => { | |
| for (const selector of selectors) { | |
| const keyA = selector(a); | |
| const keyB = selector(b); | |
| if (keyA < keyB) return -1; | |
| if (keyA > keyB) return 1; | |
| } | |
| return 0; | |
| }); | |
| return result; | |
| } | |
| /** | |
| * Pipes the array through a functional transformation. | |
| * Useful for custom operators. | |
| * @param fn - Function receiving this array and returning something. | |
| */ | |
| pipe<U>(fn: (source: QueryableArray<T>) => U): U { | |
| return fn(this); | |
| } | |
| /** | |
| * Creates a new QueryableArray instance from an array-like or iterable object. | |
| * @template T - The type of the elements in the source array. | |
| * @param arrayLike - An array-like or iterable object to convert to a QueryableArray. | |
| * @returns A new QueryableArray instance. | |
| */ | |
| static from<T>(arrayLike: ArrayLike<T> | Iterable<T>): QueryableArray<T> { | |
| return new QueryableArray<T>(...Array.from(arrayLike)); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment