import { ArrayLike, toArray } from './Array'; import { Constructor, on, use } from './Constructor'; import type { Json } from './Json'; import { isArray, isDefined, isEmpty } from './Is'; import { isA } from './IsA'; import { Get, GetProperty, ofGet, ofProperty } from './Get'; import type { Id } from './Id'; import { equals } from './Text'; import { Optional } from './Types'; import { ifDefined, ifTrue } from '../utils/If'; import { AnyKey } from './AnyKey'; import { traverse } from '../utils/Traverse'; import { Indexed } from './Indexed'; type Pred = (value: T, index: number, obj: T[]) => unknown; export class List extends Array { get ids(): List { return this.mapDefined(i => (i as any).id as Id); } get indexed(): List> { return this.map((v, index) => ({ ...v, index })); } isSubSetOf(...items: ArrayLike): boolean { return this.diff(items).length === 0; } isSuperSetOf(...items: ArrayLike): boolean { return this.length > items.length && toList(...items).isSubSetOf(...this); } isIntersectingWith(...items: ArrayLike): boolean { return this.intersect(items).length > 0; } isDisjointWith(...items: ArrayLike): boolean { return !this.isIntersectingWith(...items); } areEqual(...items: ArrayLike): boolean { return this.isSubSetOf(...items) && toList(...items).isSubSetOf(...this); } asc(p: GetProperty): List { return this.slice().sort((e1, e2) => (ofProperty(e1, p) > ofProperty(e2, p) ? 1 : -1)); } desc(p: GetProperty): List { return this.slice().sort((e1, e2) => (ofProperty(e1, p) < ofProperty(e2, p) ? 1 : -1)); } first(p?: Pred, params?: unknown): T { return (p ? this.find(p, params) : this[0]) as T; } firstValue(f: (t: T) => V, alt?: Get): Optional { return ifDefined( this.first(t => !!f(t)), f, v => ofGet(alt, v) ); } isFirst(value: T): boolean { return value === this.first(); } next(p?: Pred, params?: unknown): T { return p ? this[this.findIndex(p, params) + 1] : this[0]; } prev(p?: Pred, params?: unknown): T { return p ? this[this.findIndex(p, params) - 1] : this[0]; } last(p?: Pred, params?: unknown): T { return p ? this.filter(p, params).last() : this[this.length - 1]; } isLast(value: T): boolean { return value === this.last(); } overlaps(...items: ArrayLike): boolean { return toList(...items).some(i => this.some(t => i === t)); } diff(others: ArrayLike): List { return this.filter(i => !others.includes(i)); } diffByKey(others: ArrayLike, key: keyof T & keyof U): List { return this.filter((i: any) => !others.some((o: any) => o[key] === i[key])); } symmetricDiff(others: ArrayLike): List { return this.diff(others).concat(toList(others as T[]).diff(this)); } symmetricDiffByKey(others: ArrayLike, key: keyof T): List { return this.diffByKey(others, key).concat(toList(others as T[]).diffByKey(this, key)); } intersect(others: ArrayLike): List { return this.filter(i => others.includes(i)); } intersectByKey(others: ArrayLike, key: keyof T & keyof U): List { return this.filter((i: any) => others.some((o: any) => o[key] === i[key])); } intersectBy(others: ArrayLike, f: (value: T, value2: U) => boolean): List { return this.filter(i => others.some(o => f(i, o as U))); } accumulate(...keys: (keyof T)[]): List { return this.map((d, i, arr) => { const acc = keys.reduce((acc, v) => { (acc as any)[v] = i === 0 ? d[v] : (arr[i - 1][v] as number) + (d[v] as number); return acc; }, d); arr[i] = acc; return acc; }); } toJSON(): Json[] { return this.reduce((a, i) => { a.push(JSON.parse(JSON.stringify(i ?? {}))); return a; }, new Array()); } map(f: (value: T, index: number, array: T[]) => U, params?: unknown): List { return toList(super.map(f, params)); } flatMap(f: (this: This, value: T, index: number, array: T[]) => ReadonlyArray | U, params?: This): List { return toList(super.flatMap(f, params)); } mapDefined(f: (value: T, index: number, array: T[]) => U, params?: unknown): List> { return this.map(f, params).defined(); } mapAsync(f: (i: T) => Promise): Promise> { return Promise.all(super.map(e => f(e))).then(a => toList(a)); } mapSerial(f: (i: T) => Promise): Promise> { return super.reduce((p, item) => p.then(results => on(results, async r => r.push(await f(item)))), Promise.resolve(toList())); } distinct(): List { return this.filter((i, index) => this.indexOf(i) === index); } distinctByKey(key: keyof T): List { const seen = new Set(); return this.filter(item => !seen.has(item[key]) && seen.add(item[key])); } distinctByValue(): List { const seen = new Set(); return this.filter(item => !seen.has(JSON.stringify(item)) && seen.add(JSON.stringify(item))); } filter(p: Pred, params?: unknown): List { return toList(super.filter(p, params)); } sum(p: (t: T) => number): number { return this.reduce((sum: number, i) => sum + p(i), 0); } max(p: (value: T) => any): T; max(key: keyof T): T; max(p: keyof T | ((value: T) => any)): T { return this.desc(p).first(); } min(key: keyof T): T; min(p: (value: T) => any): T; min(p: keyof T | ((value: T) => any)): T { return this.asc(p).first(); } byId(id: Id): T { return this.first(i => equals((i as any).id, id)); } byKey(key: keyof T, value: unknown): T { return this.first(i => i[key] == value); } add(...items: ArrayLike): this { super.push(...toArray(...items)); return this; } concat(...items: ConcatArray[]): List; concat(...items: (T | ConcatArray)[]): List; concat(...items: (T | ConcatArray)[]): List { return toList(super.concat(...items)); } reverse(): List { return toList(super.reverse()); } splice(start: number, deleteCount?: number): List; splice(start: number, deleteCount: number, ...items: T[]): List; splice(start: number, deleteCount: number, ...items: T[]): List { return toList(super.splice(start, deleteCount, ...items)); } remove(item: T): List { const index = this.indexOf(item); if (index > -1) { this.splice(index, 1); } return this; } move(sourceId: Id, destinationId: Id): List { return this.moveOn('id' as keyof T, sourceId, destinationId); } moveOn(key: keyof T, sourceId: Id, destinationId: Id): List { const source = this.findIndex(i => equals(i[key], sourceId)); const dest = this.findIndex(i => equals(i[key], destinationId)); return on(this.slice(), r => r.splice(dest, 0, ...r.splice(source, 1))); } replace(key: keyof T, item: T): List { const index = this.findIndex(i => i[key] === item?.[key]); ifTrue(index != -1, () => this.splice(index, 1, item)); return this; } switch(item: T): List { return this.includes(item) ? this.remove(item) : this.add(item); } switchOn(item: U, on: AnyKey): List; switchOn(item: T, on: Pred): List; switchOn(item: T, on: AnyKey | Pred): List { return use(typeof on === 'function' ? this.find(on) : use(traverse(item, on), v => this.find(i => traverse(i, on) === v)), i => i ? this.remove(i) : this.add(item) ); } defined(): List> { return this.reduce((l, v) => (isDefined(v) ? l.add(v) : l), toList>()); } toObject(key: keyof T, options: { deleteKey?: boolean } = {}): Record { return this.reduce((o: any, i) => { o[i[key]] = i; if (options.deleteKey) delete o[i[key]][key]; return o; }, {}); } toObjectList(key: keyof T): Record> { return this.reduce( (a, t) => { const k = t[key] as unknown as string | number | symbol; a[k] = a[k] ?? toList(); a[k].push(t); return a; }, {} as Record> ); } orElse(...alt: ArrayLike): Optional> { return !isEmpty(this) ? this : !isEmpty(...alt) ? toList(...alt) : undefined; } weave(insertFrom: T[], interval: number): this { for (let i = interval, n = 0; i <= this.length && n < insertFrom.length; i += interval + 1) { this.splice(i, 0, insertFrom[n++]); } return this; } slice(start?: number, end?: number): List { return toList(super.slice(start, end)); } none(p: (t: T) => boolean): boolean { return !this.some(p); } chunk(chunkSize: number): List> { return this.reduce((acc, _, index) => (index % chunkSize === 0 ? on(acc, a => a.push(this.slice(index, index + chunkSize))) : acc), toList>()); } //we needed to add U because of a Typescript issue with generics update(p: Pred, val: T | ((v: U) => T)): List { return this.map((v, i, a) => (p(v, i, a) ? ofGet(val, v, i, a) : v)); } updateFirst(p: Pred, val: T | ((v: U) => T)) { const index = this.findIndex(p); return this.update((t, i) => p(t, i, this) && i == index, val); } updateFirstById(id: Id, val: T | ((v: U) => T)) { return this.updateFirst(i => equals((i as any)?.id, id), val); } updateById(id: Id, val: T | ((v: U) => T)) { return this.update(i => equals((i as any)?.id, id), val); } groupBy(by: Pred): List> { return toList( Array.from(this.reduce((m, v, i, a) => use(by(v, i, a), key => m.set(key, (m.get(key) ?? toList()).add(v))), new Map>()).values()) ); } } export const toList = (...items: ArrayLike): List => new List().add(...items); export const isList = (l?: unknown): l is List => isDefined(l) && isArray(l) && isA>(l, 'first', 'last', 'asc', 'desc'); export const asList = (c: Constructor, items: unknown = []): List => toList(toArray(items).map(i => new c(i))); export const maxValue = (l: List, key: keyof T): T[keyof T] | undefined => l.desc(key).first()?.[key]; export const minValue = (l: List, key: keyof T): T[keyof T] | undefined => l.asc(key).first()?.[key];