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

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 }
},
}
}
}