// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. export type IndexOf = T extends Map ? T : T extends Array ? number : string; /** performs a truthy check on the value, and calls onTrue when the condition is true,and onFalse when it's not */ export function when(value: T, onTrue: (value: NonNullable) => void, onFalse: () => void = () => { /* */ }) { return value ? onTrue(>value) : onFalse(); } export interface IterableWithLinq extends Iterable { linq: IterableWithLinq; any(predicate?: (each: T) => boolean): boolean; all(predicate: (each: T) => boolean): boolean; bifurcate(predicate: (each: T) => boolean): Array>; concat(more: Iterable): IterableWithLinq; distinct(selector?: (each: T) => any): IterableWithLinq; duplicates(selector?: (each: T) => any): IterableWithLinq; first(predicate?: (each: T) => boolean): T | undefined; selectNonNullable(selector: (each: T) => V): IterableWithLinq>; select(selector: (each: T) => V): IterableWithLinq; selectAsync(selector: (each: T) => V): AsyncGenerator; selectMany(selector: (each: T) => Iterable): IterableWithLinq; where(predicate: (each: T) => boolean): IterableWithLinq; forEach(action: (each: T) => void): void; aggregate(accumulator: (current: T | A, next: T) => A, seed?: T | A, resultAction?: (result?: T | A) => A | R): T | A | R | undefined; toArray(): Array; toObject(selector: (each: T) => [V, U]): Record; results(): Promise; toRecord(keySelector: (each: T) => string, selector: (each: T) => TValue): Record; toMap(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map; groupBy(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map>; /** * Gets or sets the length of the iterable. This is a number one higher than the highest element defined in an array. */ count(): number; /** * Adds all the elements of an array separated by the specified separator string. * @param separator A string used to separate one element of an array from the next in the resulting String. If omitted, the array elements are separated with a comma. */ join(separator?: string): string; } /* eslint-disable */ function linqify(iterable: Iterable | IterableIterator): IterableWithLinq { if ((iterable)['linq'] === iterable) { return >iterable; } const r = { [Symbol.iterator]: iterable[Symbol.iterator].bind(iterable), all: all.bind(iterable), any: any.bind(iterable), bifurcate: bifurcate.bind(iterable), concat: concat.bind(iterable), distinct: distinct.bind(iterable), duplicates: duplicates.bind(iterable), first: first.bind(iterable), select: select.bind(iterable), selectMany: selectMany.bind(iterable), selectNonNullable: selectNonNullable.bind(iterable), toArray: toArray.bind(iterable), toObject: toObject.bind(iterable), where: where.bind(iterable), forEach: forEach.bind(iterable), aggregate: aggregate.bind(iterable), join: join.bind(iterable), count: len.bind(iterable), results: results.bind(iterable), toMap: toMap.bind(iterable), groupBy: groupBy.bind(iterable), selectAsync: selectAsync.bind(iterable), }; r.linq = r; return r; } function len(this: Iterable): number { return length(this); } export function keys(source: Map | null | undefined): Iterable export function keys>(source: Record | null | undefined): Iterable export function keys>(source: Array | null | undefined): Iterable export function keys(source: any | undefined | null): Iterable export function keys(source: any): Iterable { if (source) { if (Array.isArray(source)) { return >>(>source).keys(); } if (source instanceof Map) { return >(>source).keys(); } if (source instanceof Set) { throw new Error('Unable to iterate keys on a Set'); } return >>Object.keys(source); } // undefined/null return []; } /** returns an IterableWithLinq<> for keys in the collection */ function _keys(source: Map | null | undefined): IterableWithLinq function _keys>(source: Record | null | undefined): IterableWithLinq function _keys>(source: Array | null | undefined): IterableWithLinq function _keys(source: any | undefined | null): IterableWithLinq function _keys(source: any): IterableWithLinq { if (source) { if (Array.isArray(source)) { return >>linqify((>source).keys()); } if (source instanceof Map) { return >linqify((>source).keys()); } if (source instanceof Set) { throw new Error('Unable to iterate keys on a Set'); } return >>linqify((Object.keys(source))); } // undefined/null return linqify([]); } function isIterable(source: any): source is Iterable { return !!source && !!source[Symbol.iterator]; } export function values | Record | Map)>(source: (Iterable | Array | Record | Map | Set) | null | undefined): Iterable { if (source) { // map if (source instanceof Map || source instanceof Set) { return source.values(); } // any iterable source if (isIterable(source)) { return source; } // dictionary (object keys) return Object.values(source); } // null/undefined return []; } export const linq = { values: _values, entries: _entries, keys: _keys, find: _find, startsWith: _startsWith, join: _join }; /** returns an IterableWithLinq<> for values in the collection * * @note - null/undefined/empty values are considered 'empty' */ function _values(source: (Array | Record | Map | Set | Iterable) | null | undefined): IterableWithLinq { return (source) ? linqify(values(source)) : linqify([]); } export function entries | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined): Iterable<[IndexOf, T]> { if (source) { if (Array.isArray(source)) { return , T]>>source.entries(); } if (source instanceof Map) { return , T]>>source.entries(); } if (source instanceof Set) { throw new Error('Unable to iterate items on a Set (use values)'); } return , T]>>Object.entries(source); } // undefined/null return []; } /** returns an IterableWithLinq<{key,value}> for the source */ function _entries | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined): IterableWithLinq<[IndexOf, T]> { return linqify(source ? entries(source) : []) } /** returns the first value where the key equals the match value (case-insensitive) */ function _find | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined, match: string): T | undefined { return _entries(source).first(([key,]) => key.toString().localeCompare(match, undefined, { sensitivity: 'base' }) === 0)?.[1]; } /** returns the first value where the key starts with the match value (case-insensitive) */ function _startsWith | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined, match: string): T | undefined { match = match.toLowerCase(); return _entries(source).first(([key,]) => key.toString().toLowerCase().startsWith(match))?.[1]; } function _join(source: (Array | Record | Map | Set | Iterable) | null | undefined, delimiter: string): string { return source ? _values(source).join(delimiter) : ''; } export function length(source?: string | Iterable | Record | Array | Map | Set): number { if (source) { if (Array.isArray(source) || typeof (source) === 'string') { return source.length; } if (source instanceof Map || source instanceof Set) { return source.size; } if (isIterable(source)) { return [...source].length; } return source ? Object.values(source).length : 0; } return 0; } function toMap(this: Iterable, keySelector: (each: TElement) => TKey, selector: (each: TElement) => TValue): Map { const result = new Map(); for (const each of this) { result.set(keySelector(each), selector(each)); } return result; } function groupBy(this: Iterable, keySelector: (each: TElement) => TKey, selector: (each: TElement) => TValue): Map { const result = new ManyMap(); for (const each of this) { result.push(keySelector(each), selector(each)); } return result; } function any(this: Iterable, predicate?: (each: T) => boolean): boolean { for (const each of this) { if (!predicate || predicate(each)) { return true; } } return false; } function all(this: Iterable, predicate: (each: T) => boolean): boolean { for (const each of this) { if (!predicate(each)) { return false; } } return true; } function concat(this: Iterable, more: Iterable): IterableWithLinq { return linqify(function* (this: Iterable) { for (const each of this) { yield each; } for (const each of more) { yield each; } }.bind(this)()); } function select(this: Iterable, selector: (each: T) => V): IterableWithLinq { return linqify(function* (this: Iterable) { for (const each of this) { yield selector(each); } }.bind(this)()); } async function* selectAsync(this: Iterable, selector: (each: T) => Promise) { for (const each of this) { yield selector(each) } } function selectMany(this: Iterable, selector: (each: T) => Iterable): IterableWithLinq { return linqify(function* (this: Iterable) { for (const each of this) { yield* selector(each); } }.bind(this)()); } function where(this: Iterable, predicate: (each: T) => boolean): IterableWithLinq { return linqify(function* (this: Iterable) { for (const each of this) { if (predicate(each)) { yield each; } } }.bind(this)()); } function forEach(this: Iterable, action: (each: T) => void) { for (const each of this) { action(each); } } function aggregate(this: Iterable, accumulator: (current: T | A, next: T) => A, seed?: T | A, resultAction?: (result?: T | A) => A | R): T | A | R | undefined { let result: T | A | undefined = seed; for (const each of this) { if (result === undefined) { result = each; continue; } result = accumulator(result, each); } return resultAction !== undefined ? resultAction(result) : result; } function selectNonNullable(this: Iterable, selector: (each: T) => V): IterableWithLinq> { return linqify(function* (this: Iterable) { for (const each of this) { const value = selector(each); if (value) { yield >value; } } }.bind(this)()); } function nonNullable(this: Iterable): IterableWithLinq> { return linqify(function* (this: Iterable) { for (const each of this) { if (each) { yield >each; } } }.bind(this)()); } function first(this: Iterable, predicate?: (each: T) => boolean): T | undefined { for (const each of this) { if (!predicate || predicate(each)) { return each; } } return undefined; } function toArray(this: Iterable): Array { return [...this]; } function toObject(this: Iterable, selector: (each: T) => [string, V]): Record { const result : Record = {}; for (const each of this) { const [key, value] = selector(each); result[key] = value; } return result; } async function results(this: Iterable): Promise { await Promise.all([...this]); } function join(this: Iterable, separator: string): string { return [...this].join(separator); } function bifurcate(this: Iterable, predicate: (each: T) => boolean): Array> { const result = [new Array(), new Array()]; for (const each of this) { result[predicate(each) ? 0 : 1].push(each); } return result; } function distinct(this: Iterable, selector?: (each: T) => any): IterableWithLinq { const hash: Record = {}; return linqify(function* (this: Iterable) { if (!selector) { selector = i => i; } for (const each of this) { const k = JSON.stringify(selector(each)); if (!hash[k]) { hash[k] = true; yield each; } } }.bind(this)()); } function duplicates(this: Iterable, selector?: (each: T) => any): IterableWithLinq { const hash: Record = {}; return linqify(function* (this: Iterable) { if (!selector) { selector = i => i; } for (const each of this) { const k = JSON.stringify(selector(each)); if (hash[k] === undefined) { hash[k] = false; } else { if (hash[k] === false) { hash[k] = true; yield each; } } } }.bind(this)()); } /** A Map of Key: Array */ export class ManyMap extends Map> { /** * Push the value into the array at key * @param key the unique key in the map * @param value the value to push to the collection at 'key' */ push(key: K, value: V) { this.getOrDefault(key, []).push(value); } } export function countWhere(from: Iterable, predicate: (each: T) => Promise): Promise export function countWhere(from: Iterable, predicate: (each: T) => boolean): number export function countWhere(from: Iterable, predicate: (e: T) => boolean | Promise) { let v = 0; const all = []; for (const each of from) { const test = predicate(each); if (test.then) { all.push(test.then((antecedent: any) => { if (antecedent) { v++; } })); continue; } if (test) { v++; } } if (all.length) { return Promise.all(all).then(() => v); } return v; }