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.
609 lines
19 KiB
609 lines
19 KiB
type CollectionItem<T> = T
|
|
type MaybeCollectionItem<T> = CollectionItem<T> | undefined
|
|
type KeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2>
|
|
type KeyReducerFunction<T, T2> = (current: any, item: CollectionItem<T>, index: number) => T2
|
|
type CollectionFunction<T, T2> = (items: Collection<T>) => T2
|
|
type KeyOperator<T, T2> = string | KeyFunction<T, T2>
|
|
type AssociatedCollectionItem<T2, T> = { key: T2, item: CollectionItem<T> }
|
|
type CollectionComparable<T> = CollectionItem<T>[] | Collection<T>
|
|
type DeterminesEquality<T> = (item: CollectionItem<T>, other: any) => boolean
|
|
type CollectionIndex = number
|
|
type MaybeCollectionIndex = CollectionIndex | undefined
|
|
type ComparisonFunction<T> = (item: CollectionItem<T>, other_item: CollectionItem<T>) => number
|
|
|
|
import { WhereOperator, applyWhere, whereMatch } from './Where.ts'
|
|
|
|
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
|
export {
|
|
collect,
|
|
Collection,
|
|
|
|
// Types
|
|
CollectionItem,
|
|
MaybeCollectionItem,
|
|
KeyFunction,
|
|
KeyReducerFunction,
|
|
CollectionFunction,
|
|
KeyOperator,
|
|
AssociatedCollectionItem,
|
|
CollectionComparable,
|
|
DeterminesEquality,
|
|
CollectionIndex,
|
|
MaybeCollectionIndex,
|
|
ComparisonFunction,
|
|
}
|
|
|
|
class Collection<T> {
|
|
private _items: CollectionItem<T>[] = []
|
|
|
|
public static collect<T>(items: CollectionItem<T>[]): Collection<T> {
|
|
return new Collection(items)
|
|
}
|
|
|
|
public static size(size: number): Collection<undefined> {
|
|
const arr = Array(size).fill(undefined)
|
|
return new Collection<undefined>(arr)
|
|
}
|
|
|
|
public static fill<T>(size: number, item: T): Collection<T> {
|
|
const arr = Array(size).fill(item)
|
|
return new Collection<T>(arr)
|
|
}
|
|
|
|
constructor(
|
|
items?: CollectionItem<T>[]
|
|
) {
|
|
if ( items )
|
|
this._items = items
|
|
}
|
|
|
|
private _all<T2>(key: KeyOperator<T, T2>): CollectionItem<T2>[] {
|
|
let items: CollectionItem<T2>[] = []
|
|
if ( typeof key === 'function' ) {
|
|
items = this._items.map(key)
|
|
} else if ( typeof key === 'string' ) {
|
|
items = this._items.map((item: CollectionItem<T>) => (<any>item)[key])
|
|
}
|
|
return items
|
|
}
|
|
|
|
private _all_numbers<T2>(key: KeyOperator<T, T2>): number[] {
|
|
return this._all(key).map(value => Number(value))
|
|
}
|
|
|
|
private _all_associate<T2>(key: KeyOperator<T, T2>): AssociatedCollectionItem<T2, T>[] {
|
|
const assoc_items: AssociatedCollectionItem<T2, T>[] = []
|
|
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: (<any>item)[key], item })
|
|
})
|
|
}
|
|
return assoc_items
|
|
}
|
|
|
|
all(): CollectionItem<T>[] {
|
|
return [...this._items]
|
|
}
|
|
|
|
average<T2>(key?: KeyOperator<T, T2>): 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<T2>(key?: KeyOperator<T, T2>): 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<T2>(key?: KeyOperator<T, T2>): 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<any> {
|
|
const new_items: CollectionItem<T>[] = []
|
|
const items = [...this._items]
|
|
const get_layer = (current: CollectionItem<T>|CollectionItem<T>[]) => {
|
|
if ( typeof (<any>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<T2>(key: KeyOperator<T, T2>, 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<T> {
|
|
return new Collection<T>(this._items)
|
|
}
|
|
|
|
diff<T2>(items: CollectionComparable<T|T2>): Collection<T> {
|
|
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<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._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<T2>(func: KeyFunction<T, T2>): void {
|
|
this._items.map(func)
|
|
}
|
|
|
|
map<T2>(func: KeyFunction<T, T2>): Collection<T2> {
|
|
const new_items: CollectionItem<T2>[] = []
|
|
this.each(((item, index) => {
|
|
new_items.push(func(item, index))
|
|
}))
|
|
return new Collection<T2>(new_items)
|
|
}
|
|
|
|
every<T2>(func: KeyFunction<T, T2>): boolean {
|
|
return this._items.every(func)
|
|
}
|
|
|
|
everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): boolean {
|
|
const items = this._all_associate(key)
|
|
return items.every(item => whereMatch(item, operator, operand))
|
|
}
|
|
|
|
filter<T2>(func: KeyFunction<T, T2>): Collection<T> {
|
|
return new Collection(this._items.filter(func))
|
|
}
|
|
|
|
find<T2>(func: KeyFunction<T, T2>): 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<T2>(bool: boolean, then: CollectionFunction<T, T2>): Collection<T> {
|
|
if ( bool ) then(this)
|
|
return this
|
|
}
|
|
|
|
unless<T2>(bool: boolean, then: CollectionFunction<T, T2>): Collection<T> {
|
|
if ( !bool ) then(this)
|
|
return this
|
|
}
|
|
|
|
where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Collection<T> {
|
|
const items = this._all_associate(key)
|
|
return new Collection(applyWhere(items, operator, operand))
|
|
}
|
|
|
|
whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Collection<T> {
|
|
return this.diff(this.where(key, operator, operand))
|
|
}
|
|
|
|
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._all_associate(key) ) {
|
|
if ( allowed.includes(search) ) matches.push(item)
|
|
}
|
|
return new Collection(matches)
|
|
}
|
|
|
|
whereNotIn<T2>(key: KeyOperator<T, T2>, items: CollectionComparable<T2>): Collection<T> {
|
|
return this.diff(this.whereIn(key, items))
|
|
}
|
|
|
|
first(): MaybeCollectionItem<T> {
|
|
if ( this.length > 0 ) return this._items[0]
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): MaybeCollectionItem<T> {
|
|
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<T> {
|
|
return this.get(index)
|
|
}
|
|
|
|
groupBy<T2>(key: KeyOperator<T, T2>): 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<T2>(key: KeyOperator<T, T2>): 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<T>, key?: KeyOperator<T, T>): Collection<T> {
|
|
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<T> {
|
|
if ( this.length > 0 ) return this._items.reverse()[0]
|
|
}
|
|
|
|
lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): MaybeCollectionItem<T> {
|
|
const items = this.where(key, operator, operand).all()
|
|
if ( items.length > 0 ) return items.reverse()[0]
|
|
}
|
|
|
|
lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): MaybeCollectionItem<T> {
|
|
const items = this.whereNot(key, operator, operand).all()
|
|
if ( items.length > 0 ) return items.reverse()[0]
|
|
}
|
|
|
|
pluck<T2>(key: KeyOperator<T, T2>): Collection<T2> {
|
|
return new Collection<T2>(this._all(key))
|
|
}
|
|
|
|
max<T2>(key: KeyOperator<T, T2>): number {
|
|
const values = this._all_numbers(key)
|
|
return Math.max(...values)
|
|
}
|
|
|
|
whereMax<T2>(key: KeyOperator<T, T2>): Collection<T> {
|
|
return this.where(key, '=', this.max(key))
|
|
}
|
|
|
|
min<T2>(key: KeyOperator<T, T2>): number {
|
|
const values = this._all_numbers(key)
|
|
return Math.min(...values)
|
|
}
|
|
|
|
whereMin<T2>(key: KeyOperator<T, T2>): Collection<T> {
|
|
return this.where(key, '=', this.min(key))
|
|
}
|
|
|
|
merge<T2>(items: CollectionComparable<T2>): Collection<T|T2> {
|
|
const merge = items instanceof Collection ? items.all() : items
|
|
return new Collection([...this._items, ...merge])
|
|
}
|
|
|
|
nth(n: number): Collection<T> {
|
|
const matches: CollectionItem<T>[] = []
|
|
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<T2>(func: CollectionFunction<T, T2>): any {
|
|
return func(this)
|
|
}
|
|
|
|
pop(): MaybeCollectionItem<T> {
|
|
if ( this.length > 0 ) {
|
|
return this._items.pop()
|
|
}
|
|
}
|
|
|
|
prepend(item: CollectionItem<T>): Collection<T> {
|
|
this._items = [item, ...this._items]
|
|
return this
|
|
}
|
|
|
|
push(item: CollectionItem<T>): Collection<T> {
|
|
this._items.push(item)
|
|
return this
|
|
}
|
|
|
|
concat(items: CollectionComparable<T>): Collection<T> {
|
|
const concats = items instanceof Collection ? items.all() : items
|
|
for ( const item of concats ) {
|
|
this._items.push(item)
|
|
}
|
|
return this
|
|
}
|
|
|
|
put(index: number, item: CollectionItem<T>): Collection<T> {
|
|
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<T> {
|
|
const random_items: CollectionItem<T>[] = []
|
|
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<T2>(reducer: KeyReducerFunction<T, T2>, 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<T2>(truth_test: KeyFunction<T, T2>): Collection<T> {
|
|
const rejected = this._items.filter((item, index) => {
|
|
return !truth_test(item, index)
|
|
})
|
|
return new Collection(rejected)
|
|
}
|
|
|
|
reverse(): Collection<T> {
|
|
return new Collection([...this._items.reverse()])
|
|
}
|
|
|
|
search(item: CollectionItem<T>): MaybeCollectionIndex {
|
|
let found_index
|
|
this._items.some((possible_item, index) => {
|
|
if ( possible_item === item ) {
|
|
found_index = index
|
|
return true
|
|
}
|
|
})
|
|
return found_index
|
|
}
|
|
|
|
shift(): MaybeCollectionItem<T> {
|
|
if ( this.length > 0 ) {
|
|
return this._items.shift()
|
|
}
|
|
}
|
|
|
|
shuffle(): Collection<T> {
|
|
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<T>): Collection<T> {
|
|
const items = this._items
|
|
if ( compare_func ) items.sort(compare_func)
|
|
else items.sort()
|
|
return new Collection(items)
|
|
}
|
|
|
|
sortBy<T2>(key?: KeyOperator<T, T2>): Collection<T> {
|
|
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<T2, T>) => item.item))
|
|
}
|
|
|
|
sortDesc(compare_func?: ComparisonFunction<T>): Collection<T> {
|
|
return this.sort(compare_func).reverse()
|
|
}
|
|
|
|
sortByDesc<T2>(key?: KeyOperator<T, T2>): Collection<T> {
|
|
return this.sortBy(key).reverse()
|
|
}
|
|
|
|
splice(start: CollectionIndex, deleteCount?: number): Collection<T> {
|
|
return new Collection([...this._items].splice(start, deleteCount))
|
|
}
|
|
|
|
sum<T2>(key?: KeyOperator<T, T2>): 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<T> {
|
|
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<T2>(func: CollectionFunction<T, T2>): Collection<T> {
|
|
func(this)
|
|
return this
|
|
}
|
|
|
|
unique<T2>(key?: KeyOperator<T, T2>): Collection<T|T2> {
|
|
const has: CollectionItem<T|T2>[] = []
|
|
let items
|
|
if ( key ) items = this._all<T2>(key)
|
|
else items = [...this._items]
|
|
for ( const item of items ) {
|
|
if ( !has.includes(item) ) has.push(item)
|
|
}
|
|
return new Collection(has)
|
|
}
|
|
|
|
includes(item: CollectionItem<T>): boolean {
|
|
return this._items.includes(item)
|
|
}
|
|
|
|
pad(length: number, value: CollectionItem<T>): Collection<T> {
|
|
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 }
|
|
},
|
|
}
|
|
}
|
|
}
|