/** * Returns the last element of an array. * @param array The array. * @param n Which element from the end (default is zero). */ export function tail(array: ArrayLike, n: number = 0): T { return array[array.length - (1 + n)]; } export function tail2(arr: T[]): [T[], T] { if (arr.length === 0) { throw new Error('Invalid tail call'); } return [arr.slice(0, arr.length - 1), arr[arr.length - 1]]; } export function equals( one: ReadonlyArray | undefined, other: ReadonlyArray | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b ): boolean { if (one === other) { return true; } if (!one || !other) { return false; } if (one.length !== other.length) { return false; } for (let i = 0, len = one.length; i < len; i++) { if (!itemEquals(one[i], other[i])) { return false; } } return true; } export function binarySearch( array: ReadonlyArray, key: T, comparator: (op1: T, op2: T) => number ): number { let low = 0, high = array.length - 1; while (low <= high) { const mid = ((low + high) / 2) | 0; const comp = comparator(array[mid], key); if (comp < 0) { low = mid + 1; } else if (comp > 0) { high = mid - 1; } else { return mid; } } return -(low + 1); } /** * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false * are located before all elements where p(x) is true. * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. */ export function findFirstInSorted( array: ReadonlyArray, p: (x: T) => boolean ): number { let low = 0, high = array.length; if (high === 0) { return 0; // no children } while (low < high) { const mid = Math.floor((low + high) / 2); if (p(array[mid])) { high = mid; } else { low = mid + 1; } } return low; } type Compare = (a: T, b: T) => number; export function quickSelect(nth: number, data: T[], compare: Compare): T { nth = nth | 0; if (nth >= data.length) { throw new TypeError('invalid index'); } let pivotValue = data[Math.floor(data.length * Math.random())]; let lower: T[] = []; let higher: T[] = []; let pivots: T[] = []; for (let value of data) { const val = compare(value, pivotValue); if (val < 0) { lower.push(value); } else if (val > 0) { higher.push(value); } else { pivots.push(value); } } if (nth < lower.length) { return quickSelect(nth, lower, compare); } else if (nth < lower.length + pivots.length) { return pivots[0]; } else { return quickSelect(nth - (lower.length + pivots.length), higher, compare); } } export function groupBy( data: ReadonlyArray, compare: (a: T, b: T) => number ): T[][] { const result: T[][] = []; let currentGroup: T[] | undefined = undefined; for (const element of data.slice(0).sort(compare)) { if (!currentGroup || compare(currentGroup[0], element) !== 0) { currentGroup = [element]; result.push(currentGroup); } else { currentGroup.push(element); } } return result; } /** * @returns New array with all falsy values removed. The original array IS NOT modified. */ export function coalesce(array: ReadonlyArray): T[] { return array.filter((e) => !!e); } /** * @returns false if the provided object is an array and not empty. */ export function isFalsyOrEmpty(obj: any): boolean { return !Array.isArray(obj) || obj.length === 0; } /** * @returns True if the provided object is an array and has at least one element. */ export function isNonEmptyArray(obj: T[] | undefined | null): obj is T[]; export function isNonEmptyArray( obj: readonly T[] | undefined | null ): obj is readonly T[]; export function isNonEmptyArray( obj: T[] | readonly T[] | undefined | null ): obj is T[] | readonly T[] { return Array.isArray(obj) && obj.length > 0; } /** * Removes duplicates from the given array. The optional keyFn allows to specify * how elements are checked for equalness by returning a unique string for each. */ export function distinct( array: ReadonlyArray, keyFn?: (t: T) => string ): T[] { if (!keyFn) { return array.filter((element, position) => { return array.indexOf(element) === position; }); } const seen: { [key: string]: boolean } = Object.create(null); return array.filter((elem) => { const key = keyFn(elem); if (seen[key]) { return false; } seen[key] = true; return true; }); } export function distinctES6(array: ReadonlyArray): T[] { const seen = new Set(); return array.filter((element) => { if (seen.has(element)) { return false; } seen.add(element); return true; }); } export function firstOrDefault( array: ReadonlyArray, notFoundValue: NotFound ): T | NotFound; export function firstOrDefault(array: ReadonlyArray): T | undefined; export function firstOrDefault( array: ReadonlyArray, notFoundValue?: NotFound ): T | NotFound | undefined { return array.length > 0 ? array[0] : notFoundValue; } export function flatten(arr: T[][]): T[] { return ([]).concat(...arr); } export function range(to: number): number[]; export function range(from: number, to: number): number[]; export function range(arg: number, to?: number): number[] { let from = typeof to === 'number' ? arg : 0; if (typeof to === 'number') { from = arg; } else { from = 0; to = arg; } const result: number[] = []; if (from <= to) { for (let i = from; i < to; i++) { result.push(i); } } else { for (let i = from; i > to; i--) { result.push(i); } } return result; } /** * Insert `insertArr` inside `target` at `insertIndex`. * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array */ export function arrayInsert( target: T[], insertIndex: number, insertArr: T[] ): T[] { const before = target.slice(0, insertIndex); const after = target.slice(insertIndex); return before.concat(insertArr, after); } /** * Pushes an element to the start of the array, if found. */ export function pushToStart(arr: T[], value: T): void { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); arr.unshift(value); } } /** * Pushes an element to the end of the array, if found. */ export function pushToEnd(arr: T[], value: T): void { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); arr.push(value); } } export function asArray(x: T | T[]): T[]; export function asArray(x: T | readonly T[]): readonly T[]; export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; }