type CollectionItem = T type MaybeCollectionItem = CollectionItem | undefined type KeyFunction = (item: CollectionItem, index: number) => CollectionItem type KeyReducerFunction = (current: any, item: CollectionItem, index: number) => T2 type CollectionFunction = (items: Collection) => T2 type KeyOperator = string | KeyFunction type AssociatedCollectionItem = { key: T2, item: CollectionItem } type CollectionComparable = CollectionItem[] | Collection type DeterminesEquality = (item: CollectionItem, other: any) => boolean type CollectionIndex = number type MaybeCollectionIndex = CollectionIndex | undefined type ComparisonFunction = (item: CollectionItem, other_item: CollectionItem) => number import { WhereOperator, applyWhere, whereMatch } from './Where.ts' const collect = (items: CollectionItem[]): Collection => Collection.collect(items) export { collect, Collection, // Types CollectionItem, MaybeCollectionItem, KeyFunction, KeyReducerFunction, CollectionFunction, KeyOperator, AssociatedCollectionItem, CollectionComparable, DeterminesEquality, CollectionIndex, MaybeCollectionIndex, ComparisonFunction, } class Collection { private _items: CollectionItem[] = [] public static collect(items: CollectionItem[]): Collection { return new Collection(items) } public static size(size: number): Collection { const arr = Array(size).fill(undefined) return new Collection(arr) } public static fill(size: number, item: T): Collection { const arr = Array(size).fill(item) return new Collection(arr) } constructor( items?: CollectionItem[] ) { if ( items ) this._items = items } private _all(key: KeyOperator): CollectionItem[] { let items: CollectionItem[] = [] if ( typeof key === 'function' ) { items = this._items.map(key) } else if ( typeof key === 'string' ) { items = this._items.map((item: CollectionItem) => (item)[key]) } return items } private _all_numbers(key: KeyOperator): number[] { return this._all(key).map(value => Number(value)) } private _all_associate(key: KeyOperator): AssociatedCollectionItem[] { const assoc_items: AssociatedCollectionItem[] = [] let items = [...this._items] if ( typeof key === 'function' ) { items.map((item, index) => { const key_item = key(item, index) assoc_items.push({ key: key_item, item }) }) } else if ( typeof key === 'string' ) { items.map((item, index) => { assoc_items.push({ key: (item)[key], item }) }) } return assoc_items } all(): CollectionItem[] { return [...this._items] } average(key?: KeyOperator): number { let items if ( key ) items = this._all_numbers(key) else items = this._items.map(x => Number(x)) if ( items.length === 0 ) return 0 let sum = items.reduce((prev, curr) => prev + curr) return sum / items.length } median(key?: KeyOperator): number { let items if ( key ) items = this._all_numbers(key).sort((a, b) => a - b) else items = this._items.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 } mode(key?: KeyOperator): number { let items if ( key ) items = this._all_numbers(key).sort((a, b) => a - b) else items = this._items.map(x => Number(x)).sort((a, b) => a - b) let 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(): Collection { const new_items: CollectionItem[] = [] const items = [...this._items] const get_layer = (current: CollectionItem|CollectionItem[]) => { if ( typeof (current)[Symbol.iterator] === 'function' ) { // @ts-ignore // TODO fix this for (const item of current) { if (Array.isArray(item)) { get_layer(item) } else { new_items.push(item) } } } } get_layer(items) return new Collection(new_items) } contains(key: KeyOperator, operator: WhereOperator, operand?: any): boolean { const associate = this._all_associate(key) const matches = applyWhere(associate, operator, operand) return matches.length > 0 } // TODO crossJoin clone(): Collection { return new Collection(this._items) } diff(items: CollectionComparable): Collection { const exclude = items instanceof Collection ? items.all() : items const matches = [] for ( const item of [...this._items] ) { if ( !exclude.includes(item) ) matches.push(item) } return new Collection(matches) } diffUsing(items: CollectionComparable, compare: DeterminesEquality): Collection { const exclude = items instanceof Collection ? items.all() : items const matches = [] for ( const item of [...this._items] ) { if ( !exclude.some(exc => compare(item, exc)) ) matches.push(item) } return new Collection(matches) } some(func: (item: T) => boolean): boolean { return this._items.some(func) } each(func: KeyFunction): void { this._items.map(func) } map(func: KeyFunction): Collection { const new_items: CollectionItem[] = [] this.each(((item, index) => { new_items.push(func(item, index)) })) return new Collection(new_items) } every(func: KeyFunction): boolean { return this._items.every(func) } everyWhere(key: KeyOperator, operator: WhereOperator, operand?: any): boolean { const items = this._all_associate(key) return items.every(item => whereMatch(item, operator, operand)) } filter(func: KeyFunction): Collection { return new Collection(this._items.filter(func)) } find(func: KeyFunction): number | undefined { let found_index: number | undefined = undefined this._items.some((item, index) => { if ( func(item, index) ) { found_index = index return true } }) return found_index } when(bool: boolean, then: CollectionFunction): Collection { if ( bool ) then(this) return this } unless(bool: boolean, then: CollectionFunction): Collection { if ( !bool ) then(this) return this } where(key: KeyOperator, operator: WhereOperator, operand?: any): Collection { const items = this._all_associate(key) return new Collection(applyWhere(items, operator, operand)) } whereNot(key: KeyOperator, operator: WhereOperator, operand?: any): Collection { return this.diff(this.where(key, operator, operand)) } whereIn(key: KeyOperator, items: CollectionComparable): Collection { const allowed = items instanceof Collection ? items.all() : items const matches = [] for ( const { key: search, item } of this._all_associate(key) ) { if ( allowed.includes(search) ) matches.push(item) } return new Collection(matches) } whereNotIn(key: KeyOperator, items: CollectionComparable): Collection { return this.diff(this.whereIn(key, items)) } first(): MaybeCollectionItem { if ( this.length > 0 ) return this._items[0] } firstWhere(key: KeyOperator, operator: WhereOperator = '=', operand: any = true): MaybeCollectionItem { const items = this.where(key, operator, operand).all() if ( items.length > 0 ) return items[0] } firstWhereNot(key: KeyOperator, operator: WhereOperator, operand?: any): MaybeCollectionItem { const items = this.whereNot(key, operator, operand).all() if ( items.length > 0 ) return items[0] } get length() { return this._items.length } count() { return this._items.length } // TODO flatten - depth get(index: number, fallback?: any) { if ( this.length > index ) return this._items[index] else return fallback } at(index: number): MaybeCollectionItem { return this.get(index) } groupBy(key: KeyOperator): any { const items = this._all_associate(key) const groups: any = {} for ( const item of items ) { const key = String(item.key) if ( !groups[key] ) groups[key] = [] groups[key].push(item.item) } return groups } associate(key: KeyOperator): any { const items = this._all_associate(key) const values: any = {} for ( const item of items ) { values[String(item.key)] = item.item } return values } join(delimiter: string): string { return this._items.join(delimiter) } implode(delimiter: string): string { return this.join(delimiter) } intersect(items: CollectionComparable, key?: KeyOperator): Collection { const compare = items instanceof Collection ? items.all() : items const intersect = [] let all_items if ( key ) all_items = this._all_associate(key) else all_items = this._items.map(item => { return { key: item, item } }) for ( const item of all_items ) { if ( compare.includes(item.key) ) intersect.push(item.item) } return new Collection(intersect) } isEmpty(): boolean { return this.length < 1 } isNotEmpty(): boolean { return this.length > 0 } last(): MaybeCollectionItem { if ( this.length > 0 ) return this._items.reverse()[0] } lastWhere(key: KeyOperator, operator: WhereOperator, operand?: any): MaybeCollectionItem { const items = this.where(key, operator, operand).all() if ( items.length > 0 ) return items.reverse()[0] } lastWhereNot(key: KeyOperator, operator: WhereOperator, operand?: any): MaybeCollectionItem { const items = this.whereNot(key, operator, operand).all() if ( items.length > 0 ) return items.reverse()[0] } pluck(key: KeyOperator): Collection { return new Collection(this._all(key)) } max(key: KeyOperator): number { const values = this._all_numbers(key) return Math.max(...values) } whereMax(key: KeyOperator): Collection { return this.where(key, '=', this.max(key)) } min(key: KeyOperator): number { const values = this._all_numbers(key) return Math.min(...values) } whereMin(key: KeyOperator): Collection { return this.where(key, '=', this.min(key)) } merge(items: CollectionComparable): Collection { const merge = items instanceof Collection ? items.all() : items return new Collection([...this._items, ...merge]) } nth(n: number): Collection { const matches: CollectionItem[] = [] let current = 1 this._items.forEach((item, index) => { if ( current === 1 ) matches.push(item) current += 1 if ( current > n ) current = 1 }) return new Collection(matches) } forPage(page: number, perPage: number) { const start = page * perPage - perPage const end = page * perPage return new Collection(this._items.slice(start, end)) } pipe(func: CollectionFunction): any { return func(this) } pop(): MaybeCollectionItem { if ( this.length > 0 ) { return this._items.pop() } } prepend(item: CollectionItem): Collection { this._items = [item, ...this._items] return this } push(item: CollectionItem): Collection { this._items.push(item) return this } concat(items: CollectionComparable): Collection { const concats = items instanceof Collection ? items.all() : items for ( const item of concats ) { this._items.push(item) } return this } put(index: number, item: CollectionItem): Collection { const new_items = [] let inserted = false this._items.forEach((existing, existing_index) => { if ( existing_index === index ) { new_items.push(item) inserted = true } new_items.push(existing) }) if ( !inserted ) new_items.push(item) return new Collection(new_items) } random(n: number): Collection { const random_items: CollectionItem[] = [] const all = this._items if ( n > this.length ) n = this.length while ( random_items.length < n ) { const item = all[Math.floor(Math.random() * all.length)] if ( !random_items.includes(item) ) random_items.push(item) } return new Collection(random_items) } reduce(reducer: KeyReducerFunction, initial_value?: T2): T2 | undefined { let current_value = initial_value this._items.forEach((item, index) => { current_value = reducer(current_value, item, index) }) return current_value } reject(truth_test: KeyFunction): Collection { const rejected = this._items.filter((item, index) => { return !truth_test(item, index) }) return new Collection(rejected) } reverse(): Collection { return new Collection([...this._items.reverse()]) } search(item: CollectionItem): MaybeCollectionIndex { let found_index this._items.some((possible_item, index) => { if ( possible_item === item ) { found_index = index return true } }) return found_index } shift(): MaybeCollectionItem { if ( this.length > 0 ) { return this._items.shift() } } shuffle(): Collection { const items = [...this._items] 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) } slice(start: number, end: number) { return new Collection(this._items.slice(start, end)) } // TODO split // TODO chunk sort(compare_func?: ComparisonFunction): Collection { const items = this._items if ( compare_func ) items.sort(compare_func) else items.sort() return new Collection(items) } sortBy(key?: KeyOperator): Collection { let items: any[] if ( key ) items = this._all_associate(key) else items = this._items.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) => item.item)) } sortDesc(compare_func?: ComparisonFunction): Collection { return this.sort(compare_func).reverse() } sortByDesc(key?: KeyOperator): Collection { return this.sortBy(key).reverse() } splice(start: CollectionIndex, deleteCount?: number): Collection { return new Collection([...this._items].splice(start, deleteCount)) } sum(key?: KeyOperator): number { let items if ( key ) items = this._all_numbers(key) else items = this._items.map(x => Number(x)) return items.reduce((prev, curr) => prev + curr) } take(limit: number): Collection { if ( limit === 0 ) return new Collection() else if ( limit > 0 ) { return new Collection(this._items.slice(0, limit)) } else { return new Collection(this._items.reverse().slice(0, -1 * limit).reverse()) } } tap(func: CollectionFunction): Collection { func(this) return this } unique(key?: KeyOperator): Collection { const has: CollectionItem[] = [] let items if ( key ) items = this._all(key) else items = [...this._items] for ( const item of items ) { if ( !has.includes(item) ) has.push(item) } return new Collection(has) } includes(item: CollectionItem): boolean { return this._items.includes(item) } pad(length: number, value: CollectionItem): Collection { const items = [...this._items] while ( items.length < length ) items.push(value) return new Collection(items) } toArray(): any[] { const returns: any = [] for ( const item of this._items ) { if ( item instanceof Collection ) returns.push(item.toArray()) else returns.push(item) } return returns } toJSON(replacer = undefined, space = 4): string { return JSON.stringify(this.toArray(), replacer, space) } // TODO getIterator // TODO getCachingIterator [Symbol.iterator]() { const items = this._items return { current_index: 0, next() { if ( items.length < 1 || this.current_index + 1 > items.length ) { return { done: true } } const item = items[this.current_index] this.current_index += 1 return { done: false, value: item } }, } } }