1379 lines
40 KiB
TypeScript
1379 lines
40 KiB
TypeScript
|
import {AsyncPipe, Pipeline} from '../Pipe'
|
||
|
import type {Unsubscribe, Subscription} from '../Reactive'
|
||
|
|
||
|
export type CollectionItem<T> = T
|
||
|
export type MaybeCollectionItem<T> = CollectionItem<T> | undefined
|
||
|
export type KeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2>
|
||
|
export type KeyReducerFunction<T, T2> = (current: any, item: CollectionItem<T>, index: number) => T2
|
||
|
export type CollectionFunction<T, T2> = (items: Collection<T>) => T2
|
||
|
export type KeyOperator<T, T2> = keyof T | KeyFunction<T, T2>
|
||
|
export type AssociatedCollectionItem<T2, T> = { key: T2, item: CollectionItem<T> }
|
||
|
export type CollectionComparable<T> = CollectionItem<T>[] | Collection<T>
|
||
|
export type DeterminesEquality<T> = (item: CollectionItem<T>, other: any) => boolean
|
||
|
export type CollectionIndex = number
|
||
|
export type MaybeCollectionIndex = CollectionIndex | undefined
|
||
|
export type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem<T>) => number
|
||
|
export type Collectable<T> = CollectionItem<T>[] | Collection<T>
|
||
|
|
||
|
import {type WhereOperator, applyWhere, whereMatch } from './where'
|
||
|
import type {Awaitable, Awaited, Either, Maybe, MethodsOf, MethodType} from '../types'
|
||
|
import {isLeft, right, unright} from '../types'
|
||
|
import {AsyncCollection} from './AsyncCollection'
|
||
|
import {ArrayIterable} from './ArrayIterable'
|
||
|
|
||
|
export const collect = <T>(items: CollectionItem<T>[] = []): Collection<T> => Collection.collect(items)
|
||
|
const toString = (item: unknown): string => String(item)
|
||
|
|
||
|
/**
|
||
|
* A helper class for working with arrays of items in a more robust fashion.
|
||
|
* Provides helpers for accessing sub-keys, filtering, piping, and aggregate functions.
|
||
|
*/
|
||
|
export class Collection<T> {
|
||
|
private storedItems: CollectionItem<T>[] = []
|
||
|
|
||
|
private pushSubscribers: Subscription<T>[] = []
|
||
|
|
||
|
/**
|
||
|
* Create a new collection from an array of items.
|
||
|
* @param items
|
||
|
*/
|
||
|
public static collect<T2>(items: CollectionItem<T2>[]): Collection<T2> {
|
||
|
return new Collection(items)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection from an item or array of items.
|
||
|
* Filters out undefined items.
|
||
|
* @param itemOrItems
|
||
|
*/
|
||
|
public static normalize<T2>(itemOrItems: Collection<T2> | (CollectionItem<T2>)[] | CollectionItem<T2>): Collection<T2> {
|
||
|
if ( itemOrItems instanceof Collection ) {
|
||
|
return itemOrItems
|
||
|
}
|
||
|
|
||
|
if ( !Array.isArray(itemOrItems) ) {
|
||
|
itemOrItems = [itemOrItems]
|
||
|
}
|
||
|
|
||
|
return new Collection<T2>(itemOrItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a collection of "undefined" elements of a given size.
|
||
|
* @param size
|
||
|
*/
|
||
|
public static size(size: number): Collection<undefined> {
|
||
|
const arr = Array(size).fill(undefined)
|
||
|
return new Collection<undefined>(arr)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fill a new collection of the given size with the given item.
|
||
|
* @param size
|
||
|
* @param item
|
||
|
*/
|
||
|
public static fill<T2>(size: number, item: T2): Collection<T2> {
|
||
|
const arr = Array(size).fill(item)
|
||
|
return new Collection<T2>(arr)
|
||
|
}
|
||
|
|
||
|
constructor(
|
||
|
/**
|
||
|
* The items to base the collection on.
|
||
|
*/
|
||
|
items?: CollectionItem<T>[],
|
||
|
) {
|
||
|
if ( items ) {
|
||
|
this.storedItems = items
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private allOperator<T2>(key: KeyOperator<T, T2>): CollectionItem<T2>[] {
|
||
|
let items: CollectionItem<T2>[] = []
|
||
|
if ( typeof key === 'function' ) {
|
||
|
items = this.storedItems.map(key)
|
||
|
} else {
|
||
|
items = this.storedItems.map((item: CollectionItem<T>) => (<any>item)[key])
|
||
|
}
|
||
|
return items
|
||
|
}
|
||
|
|
||
|
private allAsNumbers<T2>(key: KeyOperator<T, T2>): number[] {
|
||
|
return this.allOperator(key).map(value => Number(value))
|
||
|
}
|
||
|
|
||
|
private allAssociated<T2>(key: KeyOperator<T, T2>): AssociatedCollectionItem<T2, T>[] {
|
||
|
const associatedItems: AssociatedCollectionItem<T2, T>[] = []
|
||
|
const items = [...this.storedItems]
|
||
|
if ( typeof key === 'function' ) {
|
||
|
items.map((item, index) => {
|
||
|
const keyItem = key(item, index)
|
||
|
associatedItems.push({
|
||
|
key: keyItem,
|
||
|
item,
|
||
|
})
|
||
|
})
|
||
|
} else {
|
||
|
items.map(item => {
|
||
|
associatedItems.push({
|
||
|
key: (<any>item)[key],
|
||
|
item,
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
return associatedItems
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cast the collection to an array.
|
||
|
*/
|
||
|
all(): CollectionItem<T>[] {
|
||
|
return [...this.storedItems]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the average value of the items or one of their keys.
|
||
|
* @param key
|
||
|
*/
|
||
|
average<T2>(key?: KeyOperator<T, T2>): number {
|
||
|
let items
|
||
|
if ( key ) {
|
||
|
items = this.allAsNumbers(key)
|
||
|
} else {
|
||
|
items = this.storedItems.map(x => Number(x))
|
||
|
}
|
||
|
if ( items.length === 0 ) {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
const sum = items.reduce((prev, curr) => prev + curr)
|
||
|
return sum / items.length
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the median value of the items or one of their keys.
|
||
|
* @param key
|
||
|
*/
|
||
|
median<T2>(key?: KeyOperator<T, T2>): number {
|
||
|
let items
|
||
|
if ( key ) {
|
||
|
items = this.allAsNumbers(key).sort((a, b) => a - b)
|
||
|
} else {
|
||
|
items = this.storedItems.map(x => Number(x)).sort((a, b) => a - b)
|
||
|
}
|
||
|
|
||
|
const middle = Math.floor((items.length - 1) / 2)
|
||
|
if ( items.length % 2 ) {
|
||
|
return items[middle]
|
||
|
} else {
|
||
|
return (items[middle] + items[middle + 1]) / 2
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the mode of the items or one of their keys.
|
||
|
* @param key
|
||
|
*/
|
||
|
mode<T2>(key?: KeyOperator<T, T2>): number {
|
||
|
let items
|
||
|
if ( key ) {
|
||
|
items = this.allAsNumbers(key).sort((a, b) => a - b)
|
||
|
} else {
|
||
|
items = this.storedItems.map(x => Number(x)).sort((a, b) => a - b)
|
||
|
}
|
||
|
|
||
|
const counts: any = {}
|
||
|
for ( const item of items ) {
|
||
|
counts[item] = (counts[item] ?? -1) + 1
|
||
|
}
|
||
|
|
||
|
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Collapse a (potentially nested) collection of items down to a single dimension.
|
||
|
*/
|
||
|
collapse(): Collection<any> {
|
||
|
const newItems: CollectionItem<T>[] = []
|
||
|
const items = [...this.storedItems]
|
||
|
const getLayer = (current: CollectionItem<T>|CollectionItem<T>[]) => {
|
||
|
if ( typeof (<any>current)[Symbol.iterator] === 'function' ) {
|
||
|
for (const item of (current as any)) {
|
||
|
if (Array.isArray(item)) {
|
||
|
getLayer(item)
|
||
|
} else {
|
||
|
newItems.push(item)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getLayer(items)
|
||
|
return new Collection(newItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given key matches the given condition for any item in the collection.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const userExists = users.contains('username', '=', 'jdoe')
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): boolean {
|
||
|
const associate = this.allAssociated(key)
|
||
|
const matches = applyWhere(associate, operator, operand)
|
||
|
return matches.length > 0
|
||
|
}
|
||
|
|
||
|
// TODO crossJoin
|
||
|
|
||
|
/**
|
||
|
* Create a copy of this collection.
|
||
|
* Does NOT deep copy the underlying items.
|
||
|
*/
|
||
|
clone(): Collection<T> {
|
||
|
return new Collection<T>(this.storedItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a collection of items that ARE in this collection, but NOT in the `items` collection.
|
||
|
* @param items
|
||
|
*/
|
||
|
diff<T2>(items: CollectionComparable<T|T2>): Collection<T> {
|
||
|
const exclude = items instanceof Collection ? items.all() : items
|
||
|
const matches = []
|
||
|
for ( const item of [...this.storedItems] ) {
|
||
|
if ( !exclude.includes(item) ) {
|
||
|
matches.push(item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(matches)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Like diff, but mutates the current collection.
|
||
|
* @param items
|
||
|
*/
|
||
|
diffInPlace<T2>(items: CollectionComparable<T|T2>): this {
|
||
|
const exclude = items instanceof Collection ? items.all() : items
|
||
|
this.storedItems = this.storedItems.filter(item => !exclude.includes(item))
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a collection of items that ARE in this collection, but NOT In the `items` collection
|
||
|
* using a helper function to determine whether two items are equal.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* potentialUsers.diffUsing(storedUsers, (u1, u2) => u1.username.toLowerCase() === u2.username.toLowerCase())
|
||
|
* ```
|
||
|
*
|
||
|
* @param items
|
||
|
* @param compare
|
||
|
*/
|
||
|
diffUsing<T2>(items: CollectionComparable<T|T2>, compare: DeterminesEquality<T>): Collection<T> {
|
||
|
const exclude = items instanceof Collection ? items.all() : items
|
||
|
const matches = []
|
||
|
for ( const item of [...this.storedItems] ) {
|
||
|
if ( !exclude.some(exc => compare(item, exc)) ) {
|
||
|
matches.push(item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(matches)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given function returns truthy for any item in the collection.
|
||
|
* Stops executing if a single truth case is found.
|
||
|
* @param func
|
||
|
*/
|
||
|
some(func: (item: T) => Maybe<boolean>): boolean {
|
||
|
return this.storedItems.some(func)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute the function for every item in the collection.
|
||
|
* @param func
|
||
|
*/
|
||
|
each<T2>(func: KeyFunction<T, T2>): void {
|
||
|
this.storedItems.map(func)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection by mapping the items in this collection using the given function.
|
||
|
* @param func
|
||
|
*/
|
||
|
map<T2>(func: KeyFunction<T, T2>): Collection<T2> {
|
||
|
const newItems: CollectionItem<T2>[] = []
|
||
|
this.each(((item, index) => {
|
||
|
newItems.push(func(item, index))
|
||
|
}))
|
||
|
return new Collection<T2>(newItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection by mapping the items in this collection using the given function
|
||
|
* where the function returns an Either. The collection is all Right instances. If a Left
|
||
|
* is encountered, that value is returned.
|
||
|
* @param func
|
||
|
*/
|
||
|
mapRight<TLeft, TRight>(func: KeyFunction<T, Either<TLeft, TRight>>): Either<TLeft, Collection<TRight>> {
|
||
|
const newItems: CollectionItem<TRight>[] = []
|
||
|
for ( let i = 0; i < this.length; i += 1 ) {
|
||
|
const result = func(this.storedItems[i], i)
|
||
|
if ( isLeft(result) ) {
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
newItems.push(unright(result))
|
||
|
}
|
||
|
return right(new Collection<TRight>(newItems))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection by mapping the items in this collection using the given function
|
||
|
* where the function returns an Either. The collection is all Right instances. If a Left
|
||
|
* is encountered, that value is returned.
|
||
|
* @param func
|
||
|
*/
|
||
|
async asyncMapRight<TLeft, TRight>(func: KeyFunction<T, Awaitable<Either<TLeft, TRight>>>): Promise<Either<TLeft, Collection<TRight>>> {
|
||
|
const newItems: CollectionItem<TRight>[] = []
|
||
|
for ( let i = 0; i < this.length; i += 1 ) {
|
||
|
const result = await func(this.storedItems[i], i)
|
||
|
if ( isLeft(result) ) {
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
newItems.push(unright(result))
|
||
|
}
|
||
|
return right(new Collection<TRight>(newItems))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the collection as an AsyncCollection.
|
||
|
*/
|
||
|
toAsync(): AsyncCollection<T> {
|
||
|
const iter = new ArrayIterable([...this.storedItems])
|
||
|
return new AsyncCollection<T>(iter)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Map a method on the underlying type, passing it any required parameters.
|
||
|
* This is delightfully type-safe.
|
||
|
* @param method
|
||
|
* @param params
|
||
|
*/
|
||
|
mapCall<T2 extends MethodsOf<T>>(method: T2, ...params: Parameters<MethodType<T, T2>>): Collection<ReturnType<MethodType<T, T2>>> {
|
||
|
// This is dumb, but I'm not sure how else to resolve it. The types check out, but TypeScript loses track of the fact that
|
||
|
// typeof x[method] === MethodType<T, T2>, so it assumes we're indexing an object incorrectly.
|
||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||
|
// @ts-ignore
|
||
|
return this.map((x: T) => x[method](...params))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Shortcut for .mapCall(...).awaitAll().
|
||
|
* @param method
|
||
|
* @param params
|
||
|
*/
|
||
|
async awaitMapCall<T2 extends MethodsOf<T>>(method: T2, ...params: Parameters<MethodType<T, T2>>): Promise<Collection<Awaited<ReturnType<MethodType<T, T2>>>>> {
|
||
|
return this.mapCall(method, ...params).awaitAll()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Await all values in the collection.
|
||
|
*/
|
||
|
async awaitAll(): Promise<Collection<Awaited<T>>> {
|
||
|
return this.promiseMap(async x => x as Awaited<T>)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Map each element in the collection to a string.
|
||
|
*/
|
||
|
strings(): Collection<string> {
|
||
|
return this.map<string>(x => String(x))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection by mapping the items in this collection using the given function,
|
||
|
* excluding any for which the function returns undefined.
|
||
|
* @param func
|
||
|
*/
|
||
|
partialMap<T2>(func: KeyFunction<T, T2 | undefined>): Collection<NonNullable<T2>> {
|
||
|
const newItems: CollectionItem<NonNullable<T2>>[] = []
|
||
|
|
||
|
this.each(((item, index) => {
|
||
|
const result = func(item, index)
|
||
|
if ( typeof result !== 'undefined' ) {
|
||
|
newItems.push(result as NonNullable<T2>)
|
||
|
}
|
||
|
}))
|
||
|
|
||
|
return new Collection<NonNullable<T2>>(newItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert this collection to an object keyed by the given field.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([{uid: 1, name: 'John'}, {uid: 2, name: 'Jane'}])
|
||
|
* users.keyBy('name') // => {John: {uid: 1, name: 'John'}, Jane: {uid: 2, name: 'Jane'}}
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
*/
|
||
|
keyBy(key: KeyOperator<T, string>): {[key: string]: T} {
|
||
|
const obj: {[key: string]: T} = {}
|
||
|
|
||
|
this.allAssociated(key).forEach(assoc => {
|
||
|
obj[assoc.key] = assoc.item
|
||
|
})
|
||
|
|
||
|
return obj
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert this collection to an object keyed by the given field, whose values are
|
||
|
* the output of the `value` operator.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([{uid: 1, name: 'John'}, {uid: 2, name: 'Jane'}])
|
||
|
* users.keyMap('name', 'uid') // => {John: 1, Jane: 2}
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
* @param value
|
||
|
*/
|
||
|
keyMap<T2>(key: KeyOperator<T, string>, value: KeyOperator<T, T2>): {[key: string]: T2} {
|
||
|
const obj: {[key: string]: T2} = {}
|
||
|
|
||
|
let i = -1
|
||
|
this.allAssociated(key).forEach(assoc => {
|
||
|
i += 1
|
||
|
|
||
|
if ( typeof value === 'function' ) {
|
||
|
obj[assoc.key] = value(assoc.item, i)
|
||
|
} else {
|
||
|
obj[assoc.key] = (assoc.item[value] as any) as T2
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return obj
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given function returns true for every item in the collection.
|
||
|
* @param func
|
||
|
*/
|
||
|
every<T2>(func: KeyFunction<T, T2>): boolean {
|
||
|
return this.storedItems.every(func)
|
||
|
}
|
||
|
|
||
|
everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): boolean {
|
||
|
const items = this.allAssociated(key)
|
||
|
return items.every(item => whereMatch(item, operator, operand))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a new collection filtered by the given function.
|
||
|
* @param func
|
||
|
*/
|
||
|
filter<T2>(func?: KeyFunction<T, T2>): Collection<T> {
|
||
|
return new Collection(this.storedItems.filter(func ?? Boolean))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Like filter, but inverted. That is, removes items that DO match the criterion.
|
||
|
* @param func
|
||
|
*/
|
||
|
filterOut<T2>(func?: KeyFunction<T, T2>): Collection<T> {
|
||
|
return this.filter((...args) => !(func ?? Boolean)(...args))
|
||
|
}
|
||
|
|
||
|
whereDefined(): Collection<NonNullable<T>> {
|
||
|
return this.filter() as unknown as Collection<NonNullable<T>>
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the index of the record for which the given function returns true, if such an index exists.
|
||
|
* @param func
|
||
|
*/
|
||
|
find<T2>(func: KeyFunction<T, T2>): number | undefined {
|
||
|
let foundIndex: number | undefined = undefined
|
||
|
this.storedItems.some((item, index) => {
|
||
|
if ( func(item, index) ) {
|
||
|
foundIndex = index
|
||
|
return true
|
||
|
}
|
||
|
})
|
||
|
return foundIndex
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* When `bool` is truthy, execute the callback, passing in the collection.
|
||
|
* Can be used for functional-style chained calls.
|
||
|
* @param bool
|
||
|
* @param then
|
||
|
*/
|
||
|
when<T2>(bool: boolean, then: CollectionFunction<T, T2>): Collection<T> {
|
||
|
if ( bool ) {
|
||
|
then(this)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* When `bool` is falsy, execute the callback, passing in the collection.
|
||
|
* Can be used for functional-style chained calls.
|
||
|
* @param bool
|
||
|
* @param then
|
||
|
*/
|
||
|
unless<T2>(bool: boolean, then: CollectionFunction<T, T2>): Collection<T> {
|
||
|
if ( !bool ) {
|
||
|
then(this)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the collection by the given where-condition.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([
|
||
|
* {uid: 1, name: 'John'},
|
||
|
* {uid: 2, name: 'Jane'},
|
||
|
* {uid: 3, name: 'James'},
|
||
|
* ])
|
||
|
*
|
||
|
* users.where('uid', '<', 3) // => Collection[{uid: 1, name: 'John'}, {uid: 2, name: 'Jane'}]
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Collection<T> {
|
||
|
const items = this.allAssociated(key)
|
||
|
return new Collection(applyWhere(items, operator, operand))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the collection by the inverse of the given where-condition.
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Collection<T> {
|
||
|
return this.diff(this.where(key, operator, operand))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the collection for all records where the given key is in a set of items.
|
||
|
* @param key
|
||
|
* @param items
|
||
|
*/
|
||
|
whereIn<T2>(key: KeyOperator<T, T2>, items: CollectionComparable<T2>): Collection<T> {
|
||
|
const allowed = items instanceof Collection ? items.all() : items
|
||
|
const matches = []
|
||
|
for ( const { key: search, item } of this.allAssociated(key) ) {
|
||
|
if ( allowed.includes(search) ) {
|
||
|
matches.push(item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(matches)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the collection for all records where the given key is NOT in a set of items.
|
||
|
* @param key
|
||
|
* @param items
|
||
|
*/
|
||
|
whereNotIn<T2>(key: KeyOperator<T, T2>, items: CollectionComparable<T2>): Collection<T> {
|
||
|
return this.diff(this.whereIn(key, items))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the first item in the collection, if it exists.
|
||
|
*/
|
||
|
first(): MaybeCollectionItem<T> {
|
||
|
if ( this.length > 0 ) {
|
||
|
return this.storedItems[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the first item in the collection that matches the given where-condition.
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
firstWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): MaybeCollectionItem<T> {
|
||
|
const items = this.where(key, operator, operand).all()
|
||
|
if ( items.length > 0 ) {
|
||
|
return items[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the first item in the collection that does NOT match the given where-condition.
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): MaybeCollectionItem<T> {
|
||
|
const items = this.whereNot(key, operator, operand).all()
|
||
|
if ( items.length > 0 ) {
|
||
|
return items[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the number of items in the collection.
|
||
|
*/
|
||
|
get length(): number {
|
||
|
return this.storedItems.length
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the number of items in the collection.
|
||
|
*/
|
||
|
count(): number {
|
||
|
return this.storedItems.length
|
||
|
}
|
||
|
|
||
|
// TODO flatten - depth
|
||
|
|
||
|
/**
|
||
|
* Get the item at the given index in the collection. If none exists,
|
||
|
* return the fallback.
|
||
|
* @param index
|
||
|
* @param fallback
|
||
|
*/
|
||
|
get(index: number, fallback?: T): MaybeCollectionItem<T> {
|
||
|
if ( this.length > index ) {
|
||
|
return this.storedItems[index]
|
||
|
} else {
|
||
|
return fallback
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the item at the given index in the collection, if it exists.
|
||
|
* @param index
|
||
|
*/
|
||
|
at(index: number): MaybeCollectionItem<T> {
|
||
|
return this.get(index)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return an object mapping key values to arrays of records with that key.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([
|
||
|
* {uid: 1, name: 'John', type: 'admin'},
|
||
|
* {uid: 2, name: 'Jane', type: 'user'},
|
||
|
* {uid: 3, name: 'James', type: 'user'},
|
||
|
* ])
|
||
|
*
|
||
|
* users.groupBy('type') // => {admin: [{uid: 1, ...}], user: [{uid: 2, ...}, {uid: 3, ...}]}
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
*/
|
||
|
groupBy<T2>(key: KeyOperator<T, T2>): any {
|
||
|
const items = this.allAssociated(key)
|
||
|
const groups: any = {}
|
||
|
for ( const item of items ) {
|
||
|
const itemKey = String(item.key)
|
||
|
if ( !groups[itemKey] ) {
|
||
|
groups[itemKey] = []
|
||
|
}
|
||
|
|
||
|
groups[itemKey].push(item.item)
|
||
|
}
|
||
|
|
||
|
return groups
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return an object mapping the given key to the record with that key.
|
||
|
* See `keyBy()`.
|
||
|
* @param key
|
||
|
*/
|
||
|
associate<T2>(key: KeyOperator<T, T2>): any {
|
||
|
const items = this.allAssociated(key)
|
||
|
const values: any = {}
|
||
|
for ( const item of items ) {
|
||
|
values[String(item.key)] = item.item
|
||
|
}
|
||
|
return values
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Join the items in the collection to a string delimited by the given delimiter.
|
||
|
* @param delimiter
|
||
|
*/
|
||
|
join(delimiter: string): string {
|
||
|
return this.storedItems.join(delimiter)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Join the items in the collection to a string delimited by the given delimiter.
|
||
|
* @param delimiter
|
||
|
*/
|
||
|
implode(delimiter: string): string {
|
||
|
return this.join(delimiter)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a collection of the items that exist in both this collection and this collection,
|
||
|
* (optionally) using the given key to compare.
|
||
|
* @param items
|
||
|
* @param key
|
||
|
*/
|
||
|
intersect(items: CollectionComparable<T>, key?: KeyOperator<T, T>): Collection<T> {
|
||
|
const compare = items instanceof Collection ? items.all() : items
|
||
|
const intersect = []
|
||
|
let allItems
|
||
|
if ( key ) {
|
||
|
allItems = this.allAssociated(key)
|
||
|
} else {
|
||
|
allItems = this.storedItems.map(item => {
|
||
|
return {
|
||
|
key: item,
|
||
|
item,
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
for ( const item of allItems ) {
|
||
|
if ( compare.includes(item.key) ) {
|
||
|
intersect.push(item.item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(intersect)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* True if the collection has no items.
|
||
|
*/
|
||
|
isEmpty(): boolean {
|
||
|
return this.length < 1
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* True if the collection has at least one item.
|
||
|
*/
|
||
|
isNotEmpty(): boolean {
|
||
|
return this.length > 0
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the last item in the collection.
|
||
|
*/
|
||
|
last(): MaybeCollectionItem<T> {
|
||
|
if ( this.length > 0 ) {
|
||
|
return this.storedItems.reverse()[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the last item in the collection that matches the given where-condition.
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): MaybeCollectionItem<T> {
|
||
|
const items = this.where(key, operator, operand).all()
|
||
|
if ( items.length > 0 ) {
|
||
|
return items.reverse()[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the last item in the collection that does NOT match the given where-condition.
|
||
|
* @param key
|
||
|
* @param operator
|
||
|
* @param operand
|
||
|
*/
|
||
|
lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): MaybeCollectionItem<T> {
|
||
|
const items = this.whereNot(key, operator, operand).all()
|
||
|
if ( items.length > 0 ) {
|
||
|
return items.reverse()[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Map the collection to a collection of the values of the key.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([
|
||
|
* {uid: 1, name: 'John', type: 'admin'},
|
||
|
* {uid: 2, name: 'Jane', type: 'user'},
|
||
|
* {uid: 3, name: 'James', type: 'user'},
|
||
|
* ])
|
||
|
*
|
||
|
* users.pluck('name') // => Collection['John', 'Jane', 'James']
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
*/
|
||
|
pluck<T2 extends keyof T>(key: T2): Collection<T[T2]> {
|
||
|
return new Collection<T[T2]>(this.allOperator(key))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the max value of the given key.
|
||
|
* @param key
|
||
|
*/
|
||
|
max<T2 = T>(key: KeyOperator<T, T2>): number {
|
||
|
const values = this.allAsNumbers(key)
|
||
|
return Math.max(...values)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the item with the max value of the given key.
|
||
|
* @param key
|
||
|
*/
|
||
|
whereMax<T2>(key: KeyOperator<T, T2>): Collection<T> {
|
||
|
return this.where(key, '=', this.max(key))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the min value of the given key.
|
||
|
* @param key
|
||
|
*/
|
||
|
min<T2>(key: KeyOperator<T, T2>): number {
|
||
|
const values = this.allAsNumbers(key)
|
||
|
return Math.min(...values)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the item with the min value of the given key.
|
||
|
* @param key
|
||
|
*/
|
||
|
whereMin<T2>(key: KeyOperator<T, T2>): Collection<T> {
|
||
|
return this.where(key, '=', this.min(key))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a new collection containing both the items in this collection, and the `items` collection.
|
||
|
* @param items
|
||
|
*/
|
||
|
merge<T2>(items: CollectionComparable<T2>): Collection<T|T2> {
|
||
|
const merge = items instanceof Collection ? items.all() : items
|
||
|
return new Collection([...this.storedItems, ...merge])
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return every nth item in the collection.
|
||
|
*
|
||
|
* @example
|
||
|
* ```
|
||
|
* const items = collect(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'])
|
||
|
*
|
||
|
* items.nth(3) // => Collection['a', 'd', 'g']
|
||
|
* ```
|
||
|
*
|
||
|
* @param n
|
||
|
*/
|
||
|
nth(n: number): Collection<T> {
|
||
|
const matches: CollectionItem<T>[] = []
|
||
|
let current = 1
|
||
|
this.storedItems.forEach(item => {
|
||
|
if ( current === 1 ) {
|
||
|
matches.push(item)
|
||
|
}
|
||
|
|
||
|
current += 1
|
||
|
if ( current > n ) {
|
||
|
current = 1
|
||
|
}
|
||
|
})
|
||
|
return new Collection(matches)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the items that should exist on the given page, assuming there are `perPage` many items on a single page.
|
||
|
* @param page
|
||
|
* @param perPage
|
||
|
*/
|
||
|
forPage(page: number, perPage: number): Collection<T> {
|
||
|
const start = page * perPage - perPage
|
||
|
const end = page * perPage
|
||
|
return new Collection(this.storedItems.slice(start, end))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a new Pipe of this collection.
|
||
|
*/
|
||
|
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||
|
return pipeline.apply(this)
|
||
|
}
|
||
|
|
||
|
/** Build and apply a pipeline. */
|
||
|
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||
|
return builder(Pipeline.id()).apply(this)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a new AsyncPipe of this collection.
|
||
|
*/
|
||
|
asyncPipe(): AsyncPipe<Collection<T>> {
|
||
|
return AsyncPipe.wrap(this)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove the last item from this collection.
|
||
|
*/
|
||
|
pop(): MaybeCollectionItem<T> {
|
||
|
if ( this.length > 0 ) {
|
||
|
return this.storedItems.pop()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add the given item to the beginning of this collection.
|
||
|
* @param item
|
||
|
*/
|
||
|
prepend(item: CollectionItem<T>): Collection<T> {
|
||
|
this.storedItems = [item, ...this.storedItems]
|
||
|
this.callPushSubscribers(item)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add the given item to the end of this collection.
|
||
|
* @param item
|
||
|
*/
|
||
|
push(item: CollectionItem<T>): Collection<T> {
|
||
|
this.storedItems.push(item)
|
||
|
this.callPushSubscribers(item)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Subscribe to listen for items being added to the collection.
|
||
|
* @param sub
|
||
|
*/
|
||
|
push$(sub: Subscription<T>): Unsubscribe {
|
||
|
this.pushSubscribers.push(sub)
|
||
|
return {
|
||
|
unsubscribe: () => this.pushSubscribers = this.pushSubscribers.filter(x => x !== sub),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Helper to notify subscribers that an item has been pushed to the collection. */
|
||
|
private callPushSubscribers(item: T): void {
|
||
|
this.pushSubscribers
|
||
|
.forEach(sub => {
|
||
|
if ( typeof sub === 'object' ) {
|
||
|
sub?.next?.(item)
|
||
|
} else {
|
||
|
sub(item)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Push the given items to the end of this collection.
|
||
|
* Unlike `merge()`, this mutates the current collection's items.
|
||
|
* @param items
|
||
|
*/
|
||
|
concat(items: CollectionComparable<T>): Collection<T> {
|
||
|
const concats = items instanceof Collection ? items.all() : items
|
||
|
for ( const item of concats ) {
|
||
|
this.storedItems.push(item)
|
||
|
this.callPushSubscribers(item)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Insert the given item into this collection at the specified index.
|
||
|
* @param index
|
||
|
* @param item
|
||
|
*/
|
||
|
put(index: number, item: CollectionItem<T>): Collection<T> {
|
||
|
const newItems = []
|
||
|
let inserted = false
|
||
|
this.storedItems.forEach((existing, existingIndex) => {
|
||
|
if ( existingIndex === index ) {
|
||
|
newItems.push(item)
|
||
|
inserted = true
|
||
|
}
|
||
|
|
||
|
newItems.push(existing)
|
||
|
})
|
||
|
|
||
|
if ( !inserted ) {
|
||
|
newItems.push(item)
|
||
|
}
|
||
|
return new Collection(newItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return `n` many randomly-selected items from this collection.
|
||
|
* @param n
|
||
|
*/
|
||
|
random(n: number): Collection<T> {
|
||
|
const randomItems: CollectionItem<T>[] = []
|
||
|
const all = this.storedItems
|
||
|
if ( n > this.length ) {
|
||
|
n = this.length
|
||
|
}
|
||
|
while ( randomItems.length < n ) {
|
||
|
const item = all[Math.floor(Math.random() * all.length)]
|
||
|
if ( !randomItems.includes(item) ) {
|
||
|
randomItems.push(item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(randomItems)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reduce this collection to a single value using the given reducer function.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const items = collect([1, 3, 5, 7])
|
||
|
*
|
||
|
* items.reduce((sum, item) => sum + item, 0) // => 16
|
||
|
* ```
|
||
|
*
|
||
|
* @param reducer
|
||
|
* @param initialValue
|
||
|
*/
|
||
|
reduce<T2>(reducer: KeyReducerFunction<T, T2>, initialValue: T2): T2
|
||
|
|
||
|
reduce<T2>(reducer: KeyReducerFunction<T, T2>, initialValue?: T2): T2 | undefined {
|
||
|
let currentValue = initialValue
|
||
|
this.storedItems.forEach((item, index) => {
|
||
|
currentValue = reducer(currentValue, item, index)
|
||
|
})
|
||
|
|
||
|
return currentValue
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a new collection of items that fail the given truth-test function.
|
||
|
* @param truthTestFunction
|
||
|
*/
|
||
|
reject<T2>(truthTestFunction: KeyFunction<T, T2>): Collection<T> {
|
||
|
const rejected = this.storedItems.filter((item, index) => {
|
||
|
return !truthTestFunction(item, index)
|
||
|
})
|
||
|
return new Collection(rejected)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a new collection whose items are in the reverse order of the current one.
|
||
|
*/
|
||
|
reverse(): Collection<T> {
|
||
|
return new Collection([...this.storedItems.reverse()])
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Try to find the given item in the collection. If it exists, return the index.
|
||
|
* @param item
|
||
|
*/
|
||
|
search(item: CollectionItem<T>): MaybeCollectionIndex {
|
||
|
let foundIndex
|
||
|
this.storedItems.some((possibleItem, index) => {
|
||
|
if ( possibleItem === item ) {
|
||
|
foundIndex = index
|
||
|
return true
|
||
|
}
|
||
|
})
|
||
|
return foundIndex
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove and return the first item in the collection, if it exists.
|
||
|
*/
|
||
|
shift(): MaybeCollectionItem<T> {
|
||
|
if ( this.length > 0 ) {
|
||
|
return this.storedItems.shift()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Shuffle the items in this collection to a random order.
|
||
|
*/
|
||
|
shuffle(): Collection<T> {
|
||
|
const items = [...this.storedItems]
|
||
|
for ( let i = items.length - 1; i > 0; i-- ) {
|
||
|
const j = Math.floor(Math.random() * (i + 1))
|
||
|
;[items[i], items[j]] = [items[j], items[i]]
|
||
|
}
|
||
|
return new Collection(items)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a sub-set of this collection between the given index ranges.
|
||
|
* @param start
|
||
|
* @param end
|
||
|
*/
|
||
|
slice(start: number, end: number): Collection<T> {
|
||
|
return new Collection(this.storedItems.slice(start, end))
|
||
|
}
|
||
|
|
||
|
// TODO split
|
||
|
// TODO chunk
|
||
|
|
||
|
/**
|
||
|
* Sort the collection (optionally) using the given comparison function.
|
||
|
* @param comparisonFunction
|
||
|
*/
|
||
|
sort(comparisonFunction?: ComparisonFunction<T>): Collection<T> {
|
||
|
const items = this.storedItems
|
||
|
if ( comparisonFunction ) {
|
||
|
items.sort(comparisonFunction)
|
||
|
} else {
|
||
|
items.sort()
|
||
|
}
|
||
|
|
||
|
return new Collection(items)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sort the collection (optionally) using the given key operator.
|
||
|
* @param key
|
||
|
*/
|
||
|
sortBy<T2>(key?: KeyOperator<T, T2>): Collection<T> {
|
||
|
let items: any[]
|
||
|
if ( key ) {
|
||
|
items = this.allAssociated(key)
|
||
|
} else {
|
||
|
items = this.storedItems.map(item => {
|
||
|
return { key: item,
|
||
|
item }
|
||
|
})
|
||
|
}
|
||
|
|
||
|
items.sort((a: any, b: any) => {
|
||
|
if ( a.key > b.key ) {
|
||
|
return 1
|
||
|
} else if ( a.key < b.key ) {
|
||
|
return -1
|
||
|
} else {
|
||
|
return 0
|
||
|
}
|
||
|
})
|
||
|
return new Collection(items.map((item: AssociatedCollectionItem<T2, T>) => item.item))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Identical to `sort()`, but in reverse order.
|
||
|
* @param comparisonFunction
|
||
|
*/
|
||
|
sortDesc(comparisonFunction?: ComparisonFunction<T>): Collection<T> {
|
||
|
return this.sort(comparisonFunction).reverse()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Identical to `sortBy()`, but in reverse order.
|
||
|
* @param key
|
||
|
*/
|
||
|
sortByDesc<T2>(key?: KeyOperator<T, T2>): Collection<T> {
|
||
|
return this.sortBy(key).reverse()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove `deleteCount` many items from the collection, starting at the `start` index.
|
||
|
* @param start
|
||
|
* @param deleteCount
|
||
|
*/
|
||
|
splice(start: CollectionIndex, deleteCount?: number): Collection<T> {
|
||
|
return new Collection([...this.storedItems].splice(start, deleteCount))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return the sum of the items in the collection, optionally by key.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const items = collect([{k1: 1}, {k1: 3}, {k1: 5}, {k1: 7}])
|
||
|
*
|
||
|
* items.sum('k1') // => 16
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
*/
|
||
|
sum<T2>(key?: KeyOperator<T, T2>): number {
|
||
|
let items
|
||
|
if ( key ) {
|
||
|
items = this.allAsNumbers(key)
|
||
|
} else {
|
||
|
items = this.storedItems.map(x => Number(x))
|
||
|
}
|
||
|
return items.reduce((prev, curr) => prev + curr)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return a collection of the first `limit` many items in this collection.
|
||
|
* If `limit` is negative, returns the last `limit` many items.
|
||
|
* @param limit
|
||
|
*/
|
||
|
take(limit: number): Collection<T> {
|
||
|
if ( limit === 0 ) {
|
||
|
return new Collection()
|
||
|
} else if ( limit > 0 ) {
|
||
|
return new Collection(this.storedItems.slice(0, limit))
|
||
|
} else {
|
||
|
return new Collection(this.storedItems.reverse().slice(0, -1 * limit)
|
||
|
.reverse())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply the given function to this collection then return the collection.
|
||
|
* This is intended to help with chaining.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* collection.tap(coll => {
|
||
|
* coll.push(item)
|
||
|
* })
|
||
|
* .where('someKey', '>', 4)
|
||
|
* // ... &c.
|
||
|
* ```
|
||
|
*
|
||
|
* @param func
|
||
|
*/
|
||
|
tap<T2>(func: CollectionFunction<T, T2>): this {
|
||
|
func(this)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return all distinct items in this collection. If a key is specified, returns
|
||
|
* all unique values of that key.
|
||
|
*
|
||
|
* @example
|
||
|
* ```typescript
|
||
|
* const users = collect([
|
||
|
* {uid: 1, name: 'John', type: 'admin'},
|
||
|
* {uid: 2, name: 'Jane', type: 'user'},
|
||
|
* {uid: 3, name: 'James', type: 'user'},
|
||
|
* ])
|
||
|
*
|
||
|
* users.unique('type') // => Collection['admin', 'user']
|
||
|
* ```
|
||
|
*
|
||
|
* @param key
|
||
|
*/
|
||
|
unique<T2>(key?: KeyOperator<T, T2>): Collection<T|T2> {
|
||
|
const has: CollectionItem<T|T2>[] = []
|
||
|
let items
|
||
|
if ( key ) {
|
||
|
items = this.allOperator<T2>(key)
|
||
|
} else {
|
||
|
items = [...this.storedItems]
|
||
|
}
|
||
|
for ( const item of items ) {
|
||
|
if ( !has.includes(item) ) {
|
||
|
has.push(item)
|
||
|
}
|
||
|
}
|
||
|
return new Collection(has)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given item is in this collection.
|
||
|
* @param item
|
||
|
*/
|
||
|
includes(item: CollectionItem<T>): boolean {
|
||
|
return this.storedItems.includes(item)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add on to the end of this collection as many `value` items as necessary until the collection is `length` long.
|
||
|
* @param length
|
||
|
* @param value
|
||
|
*/
|
||
|
pad(length: number, value: CollectionItem<T>): Collection<T> {
|
||
|
const items = [...this.storedItems]
|
||
|
while ( items.length < length ) {
|
||
|
items.push(value)
|
||
|
}
|
||
|
return new Collection(items)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cast the collection to an array.
|
||
|
*/
|
||
|
toArray(recursive = true): any[] {
|
||
|
const returns: any = []
|
||
|
for ( const item of this.storedItems ) {
|
||
|
if ( recursive && item instanceof Collection ) {
|
||
|
returns.push(item.toArray())
|
||
|
} else {
|
||
|
returns.push(item)
|
||
|
}
|
||
|
}
|
||
|
return returns
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cast the collection to a JSON string, optionally specifying the replacer and indentation.
|
||
|
* @param replacer
|
||
|
* @param space
|
||
|
*/
|
||
|
toJSON(replacer = undefined, space = 4): string {
|
||
|
return JSON.stringify(this.toArray(), replacer, space)
|
||
|
}
|
||
|
|
||
|
// TODO getIterator
|
||
|
// TODO getCachingIterator
|
||
|
|
||
|
[Symbol.iterator](): Iterator<T> {
|
||
|
const items = this.storedItems
|
||
|
let currentIndex = 0
|
||
|
return {
|
||
|
next() {
|
||
|
const item = items[currentIndex]
|
||
|
currentIndex += 1
|
||
|
|
||
|
return {
|
||
|
done: currentIndex > items.length,
|
||
|
value: item,
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Like `map()`, but the callback can be async.
|
||
|
*
|
||
|
* @example
|
||
|
* A trivial example, but demonstrative:
|
||
|
*
|
||
|
* ```typescript
|
||
|
* const collection = collect([1, 2, 3])
|
||
|
*
|
||
|
* collection.map(async item => item + 1) // => Collection[Promise<1>, Promise<2>, Promise<3>]
|
||
|
*
|
||
|
* collection.promiseMap(async item => item + 1) // => Promise<Collection[2, 3, 4]>
|
||
|
* ```
|
||
|
*
|
||
|
* @param func
|
||
|
*/
|
||
|
async promiseMap<T2>(func: KeyFunction<T, T2 | Promise<T2>>): Promise<Collection<T2>> {
|
||
|
return new Collection<T2>(await Promise.all(
|
||
|
this.map(func).toArray(),
|
||
|
))
|
||
|
}
|
||
|
}
|