Standard libraries that lift up your code.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1148 lines
36 KiB

import {
AssociatedCollectionItem,
Collection, CollectionIndex,
CollectionItem, ComparisonFunction,
DeterminesEquality, KeyFunction,
KeyOperator, KeyReducerFunction, MaybeCollectionIndex, MaybeCollectionItem,
} from './Collection'
import {Iterable, StopIteration} from './Iterable'
import {applyWhere, WhereOperator} from './where'
import {AsyncPipe, Pipeline} from '../support/Pipe'
import {Awaitable} from '../support/types'
type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncCollection<T>
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
/**
* Like a collection, but asynchronous.
*/
export class AsyncCollection<T> {
constructor(
/**
* Iterable of items to base this collction on.
* @type Iterable
*/
private storedItems: Iterable<T>,
/**
* Size to use when chunking results for memory-optimization.
* @type number
*/
private iteratorChunkSize: number = 1000, // TODO fix this. It's just for testing
) {}
private async inChunks(callback: (items: Collection<T>) => any): Promise<void> {
await this.storedItems.chunk(this.iteratorChunkSize, async items => {
await callback(items)
})
await this.storedItems.reset()
}
private async inChunksAll<T2>(key: KeyOperator<T, T2>, callback: (items: Collection<T2>) => any): Promise<void> {
await this.storedItems.chunk(this.iteratorChunkSize, async items => {
if ( typeof key !== 'function' ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
key = x => x[key]
}
await callback(items.map(key))
})
await this.storedItems.reset()
}
private async inChunksAllNumbers<T2>(key: KeyOperator<T, T2>, callback: (items: number[]) => any): Promise<void> {
await this.storedItems.chunk(this.iteratorChunkSize, async items => {
if ( typeof key !== 'function' ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
key = x => x[key]
}
await callback(items.map(key).map(x => Number(x))
.all())
})
await this.storedItems.reset()
}
private async inChunksAllAssociated<T2>(key: KeyOperator<T, T2>, callback: (items: AssociatedCollectionItem<T2, T>[]) => any): Promise<void> {
await this.storedItems.chunk(this.iteratorChunkSize, async items => {
const assocItems: AssociatedCollectionItem<T2, T>[] = []
if ( typeof key === 'function' ) {
items.map((item, index) => {
const keyItem = key(item, index)
assocItems.push({ key: keyItem,
item })
})
} else {
items.map(item => {
assocItems.push({key: (<any>item)[key],
item})
})
}
await callback(assocItems)
})
await this.storedItems.reset()
}
/**
* Get all items in this collection as an array.
* @return Promise<array>
*/
async all(): Promise<CollectionItem<T>[]> {
return (await this.storedItems.all()).toArray()
}
/**
* Get all items in this collection as a synchronous Collection
* @return Promise<Collection>
*/
async collect(): Promise<Collection<T>> {
return this.storedItems.all()
}
/**
* Get the average value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async average<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let runningTotal = 0
let runningItems = 0
const chunkHelper = (items: number[]) => {
runningItems += items.length
runningTotal += items.reduce((prev, curr) => prev + curr)
}
if ( key ) {
await this.inChunksAllNumbers(key, chunkHelper)
} else {
await this.inChunks((items) => {
chunkHelper(items.map(x => Number(x)).all())
})
}
return runningTotal / runningItems
}
/**
* Get the median value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async median<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let items: number[] = []
const chunkHelper = (nextItems: number[]) => {
items = items.concat(nextItems)
}
if ( key ) {
await this.inChunksAllNumbers(key, chunkHelper)
} else {
await this.inChunks(chunkItems => {
chunkHelper(chunkItems.map(x => Number(x)).all())
})
}
items = items.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 value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async mode<T2>(key?: KeyOperator<T, T2>): Promise<number> {
const counts: any = {}
const chunkHelper = (items: number[]) => {
for ( const item of items ) {
if ( !counts[item] ) {
counts[item] = 1
} else {
counts[item] += 1
}
}
}
if ( key ) {
await this.inChunksAllNumbers(key, chunkHelper)
} else {
await this.inChunks(items => {
chunkHelper(items.map(x => Number(x)).all())
})
}
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
}
/**
* If this collection contains nested collections, collapse them to a single level.
* @return Promise<Collection>
*/
async collapse(): Promise<Collection<any>> {
const items = await this.collect()
return items.collapse() as Collection<any>
}
/**
* Returns true if the collection contains an item satisfying the given collection.
* @example
* collection.contains('id', '>', 4)
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<boolean>
*/
async contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<boolean> {
let contains = false
await this.inChunksAllAssociated(key, (items: AssociatedCollectionItem<T2, T>[]) => {
const matches = applyWhere(items, operator, operand)
if ( matches.length > 0 ) {
contains = true
throw new StopIteration()
}
})
return contains
}
/**
* Returns a clean instance of this collection pointing to the same result set of the iterable.
* @return Promise<AsyncCollection>
*/
async clone(): Promise<AsyncCollection<T>> {
return new AsyncCollection<T>(await this.storedItems.clone())
}
/**
* Returns the elements that are different between the two collections.
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
async diff(items: AsyncCollectionComparable<T>): Promise<Collection<T>> {
const matches: T[] = []
await this.inChunks(async chunk => {
for ( const item of chunk.all() ) {
if ( !(await items.includes(item)) ) {
matches.push(item)
}
}
})
return new Collection<T>(matches)
}
/**
* Returns the elements that are different between the two collections, using the given function
* as a comparator for the elements.
* @param {AsyncCollectionComparable} items
* @param {DeterminesEquality} compare
* @return Promise<Collection>
*/
async diffUsing(items: AsyncCollectionComparable<T>, compare: DeterminesEquality<T>): Promise<Collection<T>> {
const matches: T[] = []
await this.inChunks(async chunk => {
for ( const item of chunk.all() ) {
if ( !(await items.some(exc => compare(item, exc))) ) {
matches.push(item)
}
}
})
return new Collection<T>(matches)
}
/**
* Returns true if the given item is present in the collection.
* @param item
* @return Promise<boolean>
*/
async includes(item: CollectionItem<T>): Promise<boolean> {
let contains = false
await this.inChunks(items => {
if ( items.includes(item) ) {
contains = true
throw new StopIteration()
}
})
return contains
}
/**
* Returns true if there is an item in the collection for which the given operator returns true.
* @param {function} operator - item => boolean
* @return Promise<boolean>
*/
async some(operator: (item: T) => Awaitable<boolean|undefined|void>): Promise<boolean> {
let contains = false
await this.inChunks(async items => {
for ( const item of items.all() ) {
if ( await operator(item) ) {
contains = true
throw new StopIteration()
}
}
})
return contains
}
/**
* Applies a callback to each item in the collection.
* @param {AsyncKeyFunction} func
* @return Promise<void>
*/
async each<T2>(func: AsyncKeyFunction<T, T2>): Promise<void> {
let index = 0
await this.inChunks(async items => {
for ( const item of items.all() ) {
await func(item, index)
index += 1
}
})
}
/**
* Applies a callback to each item in the collection and returns the results as a collection.
* @param {AsyncKeyFunction} func
* @return Promise<Collection>
*/
async map<T2>(func: AsyncKeyFunction<T, T2>): Promise<Collection<T2>> {
const newItems: CollectionItem<T2>[] = []
await this.each(async (item, index) => {
newItems.push(await func(item, index))
})
return new Collection<T2>(newItems)
}
/**
* Create a new collection by mapping the items in this collection using the given function,
* excluding any for which the function resolves undefined.
* @param func
*/
async partialMap<T2>(func: AsyncKeyFunction<T, T2 | undefined>): Promise<Collection<NonNullable<T2>>> {
const newItems: CollectionItem<NonNullable<T2>>[] = []
await this.each(async (item, index) => {
const result = await func(item, index)
if ( typeof result !== 'undefined' ) {
newItems.push(result as unknown as NonNullable<T2>)
}
})
return new Collection<NonNullable<T2>>(newItems)
}
/**
* Returns true if the given operator returns true for every item in the collection.
* @param {AsyncKeyFunction} func
* @return Promise<boolean>
*/
async every<T2>(func: AsyncKeyFunction<T, T2>): Promise<boolean> {
let pass = true
let index = 0
await this.inChunks(async items => {
for ( const item of items.all() ) {
if ( !(await func(item, index)) ) {
pass = false
throw new StopIteration()
}
index += 1
}
})
return pass
}
/**
* Returns true if every item in the collection satisfies the given where clause.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
*/
async everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<boolean> {
let pass = true
await this.inChunks(async items => {
pass = pass && items.everyWhere(key, operator, operand)
if ( !pass ) {
throw new StopIteration()
}
})
return pass
}
/**
* Applies a filter to every item in the collection and returns the results that pass the filter.
* @param {KeyFunction} func
* @return Promise<Collection>
*/
async filter<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
let newItems: CollectionItem<T>[] = []
await this.inChunks(async items => {
const filterItems: CollectionItem<T>[] = []
for ( let i = 0; i < items.length; i += 1 ) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const item = items.get(i)!
if ( await func(item, i) ) {
filterItems.push(item)
}
}
newItems = newItems.concat(filterItems)
})
return new Collection<T>(newItems)
}
/**
* Like filter, but inverted. That is, filters out items that DO match the criterion.
* @param func
*/
async filterOut<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
return this.filter(async (...args) => !(await func(...args)))
}
/**
* Calls the passed in function if the boolean condition is true. Allows for functional syntax.
* @param {boolean} bool
* @param {AsyncCollectionFunction} then
* @return AsyncCollection
*/
when<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( bool ) {
then(this)
}
return this
}
/**
* Calls the passed in function if the boolean condition is false. Allows for functional syntax.
* @param {boolean} bool
* @param {AsyncCollectionFunction} then
* @return AsyncCollection
*/
unless<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( !bool ) {
then(this)
}
return this
}
/**
* Applies the given where condition to the collection and returns a new collection of the results.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<Collection>
*/
async where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<Collection<T>> {
let newItems: CollectionItem<T>[] = []
await this.inChunks(async items => {
newItems = newItems.concat(items.where(key, operator, operand).all())
})
return new Collection<T>(newItems)
}
/**
* Applies the given where condition to the collection and returns a new collection of the items
* that did not satisfy the condition.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<Collection>
*/
async whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<Collection<T>> {
let newItems: CollectionItem<T>[] = []
await this.inChunks(async items => {
newItems = newItems.concat(items.whereNot(key, operator, operand).all())
})
return new Collection<T>(newItems)
}
/**
* Applies a WHERE ... IN ... condition to the collection an returns a new collection of the results.
* @param {KeyOperator} key
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
async whereIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
const newItems: CollectionItem<T>[] = []
await this.inChunksAllAssociated(key, async chunk => {
for ( const item of chunk ) {
if ( await items.includes(item.key) ) {
newItems.push(item.item)
}
}
})
return new Collection<T>(newItems)
}
/**
* Applies a WHERE ... IN ... condition to the collection and returns a new collection of the items
* that did not satisfy the condition.
* @param {KeyOperator} key
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
async whereNotIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
const newItems: CollectionItem<T>[] = []
await this.inChunksAllAssociated(key, async chunk => {
for ( const item of chunk ) {
if ( !(await items.includes(item.key)) ) {
newItems.push(item.item)
}
}
})
return new Collection<T>(newItems)
}
/**
* Returns the first item in the collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async first(): Promise<MaybeCollectionItem<T>> {
return this.storedItems.at(0)
}
/**
* Return the first item in the collection that satisfies the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} [operator = '=']
* @param [operand = true]
* @return Promise<MaybeCollectionItem>
*/
async firstWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
let item = undefined
await this.inChunksAllAssociated(key, async items => {
const matches = applyWhere(items, operator, operand)
if ( matches.length > 0 ) {
item = matches[0]
throw new StopIteration()
}
})
return item
}
/**
* Return the first item in the collection that does not satisfy the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} [operator = '=']
* @param [operand = true]
*/
async firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
let item: MaybeCollectionItem<T> = undefined
await this.inChunks(async items => {
const matches = items.whereNot(key, operator, operand)
if ( matches.length > 0 ) {
item = matches.first()
throw new StopIteration()
}
})
return item
}
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async count(): Promise<number> {
return this.storedItems.count()
}
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async length(): Promise<number> {
return this.storedItems.count()
}
/**
* Get the item at the given index of this collection, if one exists.
* If none exists and a fallback value is provided, that value will be returned.
* @param {number} index
* @param [fallback]
* @return Promise<any>
*/
async get(index: number, fallback?: T): Promise<T | undefined> {
if ( (await this.count()) > index ) {
return this.storedItems.at(index)
} else {
return fallback
}
}
/**
* Get the item at the given index of this collection, if one exists.
* @param {number} index
*/
async at(index: number): Promise<MaybeCollectionItem<T>> {
return this.get(index)
}
/**
* Return an object which maps key values to arrays of items in the collection that satisfy that value.
* @param {KeyOperator} key
* @return Promise<object>
*/
async groupBy<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).groupBy(key)
}
/**
* Return an object mapping the given key value to items in this collection.
* @param {KeyOperator} key
* @return Promise<object>
*/
async associate<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).associate(key)
}
/**
* Join the items in this collection with the given delimiter.
* @example
* await collection.join(',') // => '1,2,3,4'
* @param {string} delimiter
* @return Promise<string>
*/
async join(delimiter: string): Promise<string> {
const runningStrings: string[] = []
await this.inChunks(async items => {
runningStrings.push(items.join(delimiter))
})
return runningStrings.join(delimiter)
}
/**
* Join the items in this collection with the given delimiter.
* @example
* await collection.implode(',') // => '1,2,3,4'
* @param {string} delimiter
* @return Promise<string>
*/
async implode(delimiter: string): Promise<string> {
return this.join(delimiter)
}
// TODO intersect
/**
* Returns true if there are no items in this collection.
* @return Promise<boolean>
*/
async isEmpty(): Promise<boolean> {
return (await this.storedItems.count()) < 1
}
/**
* Returns true if there is at least one item in this collection.
* @return Promise<boolean>
*/
async isNotEmpty(): Promise<boolean> {
return (await this.storedItems.count()) > 0
}
/**
* Return the last item in this collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async last(): Promise<MaybeCollectionItem<T>> {
const length = await this.storedItems.count()
if ( length > 0 ) {
return this.storedItems.at(length - 1)
}
}
/**
* Return the last item in this collection which satisfies the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<MaybeCollectionItem>
*/
async lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<MaybeCollectionItem<T>> {
return (await this.where(key, operator, operand)).last()
}
/**
* Return the last item in this collection which does not satisfy the given condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<MaybeCollectionItem>
*/
async lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: unknown): Promise<MaybeCollectionItem<T>> {
return (await this.whereNot(key, operator, operand)).last()
}
/**
* Builds a collection of the values of a given key for each item in this collection.
* @example
* // collection has { a: 1 }, { a: 2 }, { a: 3 }
* await collection.pluck('a') // => [1, 2, 3]
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async pluck<T2 extends keyof T>(key: T2): Promise<Collection<T[T2]>> {
let newItems: CollectionItem<T[T2]>[] = []
await this.inChunksAll(key, async items => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
newItems = newItems.concat(items.all())
})
return new Collection<T[T2]>(newItems)
}
/**
* Return the max value of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async max<T2>(key: KeyOperator<T, T2>): Promise<number | undefined> {
let runningMax: number | undefined = undefined
await this.inChunksAllNumbers(key, async items => {
const localMax = Math.max(...items)
if ( typeof runningMax === 'undefined' ) {
runningMax = localMax
} else {
runningMax = Math.max(runningMax, localMax)
}
})
return runningMax
}
/**
* Return a collection of items that have the max value of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async whereMax<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.max(key))
}
/**
* Return the min value of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async min<T2>(key: KeyOperator<T, T2>): Promise<number | undefined> {
let runningMin: number | undefined = undefined
await this.inChunksAllNumbers(key, async items => {
const localMin = Math.min(...items)
if ( typeof runningMin === 'undefined' ) {
runningMin = localMin
} else {
runningMin = Math.min(runningMin, localMin)
}
})
return runningMin
}
/**
* Return a collection of items that have the min value of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async whereMin<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.min(key))
}
/**
* Merge the two collections.
* @param {AsyncCollectionComparable} mergeWith
* @return Promise<Collection>
*/
async merge<T2>(mergeWith: AsyncCollectionComparable<T2>): Promise<Collection<T|T2>> {
let items: T2[] = []
if ( mergeWith instanceof Collection ) {
items = await mergeWith.all()
} else if ( mergeWith instanceof AsyncCollection ) {
items = await mergeWith.all()
} else if ( Array.isArray(mergeWith) ) {
items = mergeWith
}
return new Collection<T|T2>([...items, ...await this.all()])
}
/**
* Return a collection of every nth item in this collection.
* @param {number} n
* @return Promise<Collection>
*/
async nth(n: number): Promise<Collection<T>> {
const matches: CollectionItem<T>[] = []
let current = 1
await this.inChunks(async chunk => {
for ( const item of chunk.all() ) {
if ( current === 1 ) {
matches.push(item)
}
current += 1
if ( current > n ) {
current = 1
}
}
})
return new Collection<T>(matches)
}
/**
* Return a collection containing the items that would be on the given page, with the given number of items per page.
* @param {number} page
* @param {number} perPage
*/
async forPage(page: number, perPage: number): Promise<Collection<T>> {
const start = page * perPage - perPage
const end = page * perPage - 1
return this.storedItems.range(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<AsyncCollection<T>> {
return AsyncPipe.wrap(this)
}
/* async pop(): Promise<MaybeCollectionItem<T>> {
const nextItem = await this.storedItems.next()
if ( !nextItem.done ) {
return nextItem.value
}
}*/ // TODO Fix this
/**
* Get n random items from this collection.
* @todo add safety check for it loop exceeds max number of items
* @param {number} n
* @return Promise<Collection>
*/
async random(n: number): Promise<Collection<T>> {
const randomItems: CollectionItem<T>[] = []
const fetchedIndices: number[] = []
const maxN = await this.storedItems.count()
if ( n > maxN ) {
n = maxN
}
while ( randomItems.length < n ) {
const index = Math.floor(Math.random() * maxN)
if ( !fetchedIndices.includes(index) ) {
fetchedIndices.push(index)
const item = await this.storedItems.at(index)
if ( typeof item !== 'undefined' ) {
randomItems.push(item)
}
}
}
return new Collection<T>(randomItems)
}
/**
* Collapse the collection into a single value using a reducer function.
* @param {KeyReducerFunction} reducer
* @param [initialValue]
* @return Promise<any>
*/
async reduce<T2>(reducer: KeyReducerFunction<T, T2>, initialValue?: T2): Promise<T2 | undefined> {
let currentValue = initialValue
let index = 0
await this.inChunks(async items => {
for ( const item of items.all() ) {
currentValue = reducer(currentValue, item, index)
index += 1
}
})
return currentValue
}
/**
* Returns a collection of items that fail the truth test.
* @param {AsyncKeyFunction} truthTestFunction
* @return Promise<Collection>
*/
async reject<T2>(truthTestFunction: AsyncKeyFunction<T, T2>): Promise<Collection<T>> {
let rejected: CollectionItem<T>[] = []
await this.inChunks(async items => {
rejected = rejected.concat(items.all().filter((item, index) => {
return !truthTestFunction(item, index)
}))
})
return new Collection<T>(rejected)
}
/**
* Get a reversed collection of this collection's items.
* @return Promise<Collection>
*/
async reverse(): Promise<Collection<T>> {
return (await this.collect()).reverse()
}
/**
* Search the collection and return the index of that item, if one exists.
* @param {CollectionItem} item
* @return Promise<MaybeCollectionIndex>
*/
async search(item: CollectionItem<T>): Promise<MaybeCollectionIndex> {
let foundIndex
let index = 0
await this.inChunks(async items => {
items.some(possibleItem => {
if ( possibleItem === item ) {
foundIndex = index
throw new StopIteration()
}
index += 1
return false
})
})
return foundIndex
}
/**
* Get the next item in the collection and remove it.
* @return Promise<MaybeCollectionItem>
*/
async shift(): Promise<MaybeCollectionItem<T>> {
const nextItem = await this.storedItems.next()
if ( !nextItem.done ) {
return nextItem.value
}
}
/**
* Shuffle the items in the collection to a random order.
* @return Promise<Collection>
*/
async shuffle(): Promise<Collection<T>> {
return (await this.collect()).shuffle()
}
/**
* Return a slice of this collection.
* @param {number} start - the starting index
* @param {number} end - the ending index
* @return Promise<Collection>
*/
async slice(start: number, end: number): Promise<Collection<T>> {
return this.storedItems.range(start, end - 1)
}
/**
* Sort the collection, optionally with the given comparison function.
* @param {ComparisonFunction} comparisonFunction
* @return Promise<Collection>
*/
async sort(comparisonFunction?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sort(comparisonFunction)
}
/**
* Sort the collection by the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async sortBy<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortBy(key)
}
/**
* Reverse sort the collection, optionally with the given comparison function.
* @param {ComparisonFunction} comparisonFunction
* @return Promise<Collection>
*/
async sortDesc(comparisonFunction?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sortDesc(comparisonFunction)
}
/**
* Reverse sort the collection by the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async sortByDesc<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortByDesc(key)
}
/**
* Splice the collection at the given index. Optionally, removing the given number of items.
* @param {CollectionIndex} start
* @param {number} [deleteCount]
* @return Promise<Collection>
*/
async splice(start: CollectionIndex, deleteCount?: number): Promise<Collection<T>> {
return (await this.collect()).splice(start, deleteCount)
}
/**
* Sum the items in the collection, or the values of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async sum<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let runningSum = 0
const chunkHandler = (items: number[]) => {
for ( const item of items ) {
runningSum += item
}
}
if ( key ) {
await this.inChunksAllNumbers(key, chunkHandler)
} else {
await this.inChunks(async chunk => {
chunkHandler(chunk.map(x => Number(x)).all())
})
}
return runningSum
}
/**
* Take the first n items from the front or back of the collection.
* @param {number} limit
* @return Promise<Collection>
*/
async take(limit: number): Promise<Collection<T>> {
if ( limit === 0 ) {
return new Collection<T>()
} else if ( limit > 0 ) {
return this.slice(0, limit)
} else {
const cnt = await this.storedItems.count()
return this.storedItems.range(cnt - (-1 * limit), cnt - 1)
}
}
/**
* Call the given function, passing in this collection. Allows functional syntax.
* @param {AsyncCollectionFunction} func
* @return Promise<AsyncCollection>
*/
async tap<T2>(func: AsyncCollectionFunction<T, T2>): Promise<AsyncCollection<T>> {
await func(this)
return this
}
/**
* Return all the unique values in the collection, or the unique values of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async unique<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T|T2>> {
const has: CollectionItem<T|T2>[] = []
if ( !key ) {
await this.inChunks(async items => {
for ( const item of items.all() ) {
if ( !has.includes(item) ) {
has.push(item)
}
}
})
} else {
await this.inChunksAll(key, async items => {
for ( const item of items.all() ) {
if ( !has.includes(item) ) {
has.push(item)
}
}
})
}
return new Collection<T|T2>(has)
}
/**
* Cast this collection to an array.
* @return Promise<array>
*/
async toArray(): Promise<any[]> {
const returns: any = []
for ( const item of (await this.all()) ) {
if ( item instanceof Collection ) {
returns.push(item.toArray())
} else if ( item instanceof AsyncCollection ) {
returns.push(await item.toArray())
} else {
returns.push(item)
}
}
return returns
}
/**
* Cast this collection to a JSON string.
* @param [replacer] - the replacer to use
* @param {number} [space = 4] number of indentation spaces to use
*/
async toJSON(replacer = undefined, space = 4): Promise<string> {
return JSON.stringify(this.toArray(), replacer, space)
}
/**
* Get a clone of the underlying iterator of this collection.
* @return Iterable
*/
iterator(): Iterable<T> {
return this.storedItems.clone()
}
}