UNPKG

@eclipse-scout/core

Version:
723 lines (659 loc) 20.9 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {objects, strings} from '../index'; export const arrays = { /** * Ensures the given parameter is an array. */ ensure<T>(array: T[] | T): T[] { if (array === undefined || array === null) { return []; } if (!Array.isArray(array)) { return [array]; } return array; }, /** * Creates an array with the given length and initializes each value with the given initValue. */ init<T>(length: number, initValue: T): T[] { let array = []; for (let i = 0; i < length; i++) { array[i] = initValue; } return array; }, /** * Removes the first occurrence of the specified element from the array. * If the array does not contain the element, it stays unchanged. * * @returns true if an element was removed */ remove<T>(arr: T[], element: T): boolean { if (arr) { let index = arr.indexOf(element); if (index !== -1) { arr.splice(index, 1); return true; } } return false; }, /** * Removes the first array element that matches the given predicate. * If no element matches the given predicate, the array stays unchanged. * * @returns true if an element was removed */ removeByPredicate<T>(arr: T[], predicate: (elem: T, index: number, arr: T[]) => boolean, thisArg?: any): boolean { let index = arrays.findIndex(arr, predicate, thisArg); if (index !== -1) { arr.splice(index, 1); return true; } return false; }, /** * Removes every given element from the array * * @returns true if the array contained at least one of the specified elements */ removeAll<T>(arr: T[], elements: T[]): boolean { let modified = false; if (!elements || elements.length === 0) { return false; } for (let i = arr.length - 1; i >= 0; i--) { if (elements.indexOf(arr[i]) > -1) { arr.splice(i, 1); modified = true; } } return modified; }, /** * @returns the index of the replaced element */ replace<T>(arr: T[], element: T, replacement: T): number { let index = arr.indexOf(element); if (index !== -1) { arr[index] = replacement; } return index; }, /** * Inserts the given element at the specified index. * <p> * This function uses {@link insertAll} which relies on Array.prototype.splice(). Check its js-doc for details. */ insert<T>(arr: T[], element: T, index: number) { arrays.insertAll(arr, [element], index); }, /** * Inserts all elements of the given array at the specified index. * * This function is based on Array.prototype.splice(). * Thus, if the 'index' is greater than the length of the array, 'elements' will be added to the end of the array 'arr'. * This may cause unexpected behavior on accessing arr[index] after insertion. * * The caller must ensure the size of the array. */ insertAll<T>(arr: T[], elements: T | T[], index: number) { let elementsArr = arrays.ensure(elements); arr.splice(index, 0, ...elementsArr); }, /** * Inserts the given element into the array according to the sort order indicated by the given comparison function. * * All arguments are mandatory. */ insertSorted<T>(arr: T[], element: T, compareFunc: (a: T, b: T) => number) { // https://en.wikipedia.org/wiki/Binary_search_algorithm let left = 0; let right = arr.length - 1; while (left <= right) { let middle = left + Math.floor((right - left) / 2); let c = compareFunc(arr[middle], element); if (c < 0) { // Search in right half left = middle + 1; } else if (c > 0) { // Search in left half right = middle - 1; } else { // Found an exact match. // The insertion point index is equal to the last index starting from "middle" that matches // the element. This ensures a stable insertion order (because of the device-and-conquer // method, "middle" might be any of the elements with the same value). left = middle + 1; while (left < arr.length && compareFunc(arr[left], element) === 0) { left++; } break; } } // "left" now contains the index to insert the element arr.splice(left, 0, element); }, /** * Inserts to given element into the array directly BEFORE the first array element that matches the given predicate. * If no such element can be found, the new element is inserted at the BEGINNING of the array. * * @param thisArg optional "this" binding for predicate function */ insertBefore<T>(arr: T[], elementToInsert: T, predicate: (elem: T, index: number, arr: T[]) => boolean, thisArg?: any) { let index = arrays.findIndex(arr, predicate, thisArg); if (index === -1) { arr.unshift(elementToInsert); } else { arrays.insert(arr, elementToInsert, index); } }, /** * Inserts to given element into the array directly AFTER the first array element that matches the given predicate. * If no such element can be found, the new element is inserted at the END of the array. */ insertAfter<T>(arr: T[], elementToInsert: T, predicate: (elem: T, index: number, arr: T[]) => boolean) { let index = arrays.findIndex(arr, predicate); if (index === -1) { arr.push(elementToInsert); } else { arrays.insert(arr, elementToInsert, index + 1); } }, /** * Moves the element at fromIndex to toIndex. * * This function uses {@link insert} which relies on Array.prototype.splice(). Check its js-doc for details. */ move<T>(arr: T[], fromIndex: number, toIndex: number) { let element = arr.splice(fromIndex, 1)[0]; arrays.insert(arr, element, toIndex); }, /** * Moves the given element before the sibling and returns the array with the new order. */ moveBefore<T>(arr: T[], elementToMove: T, sibling: T): T[] { let elements = []; for (let elem of arr) { if (elem === elementToMove) { continue; } if (elem === sibling) { elements.push(elementToMove); } elements.push(elem); } return elements; }, /** * Moves the given element after the sibling and returns the array with the new order. */ moveAfter<T>(arr: T[], elementToMove: T, sibling: T): T[] { let elements = []; for (let elem of arr) { if (elem === elementToMove) { continue; } elements.push(elem); if (elem === sibling) { elements.push(elementToMove); } } return elements; }, /** * Moves the given element to the position returns the array with the new order. */ moveTo<T>(arr: T[], elementToMove: T, position: number): T[] { let elements = []; arr = arrays.ensure(arr); if (arr.length === 0) { return elements; } for (let i = 0; i < arr.length; i++) { if (i === position) { elements.push(elementToMove); } if (arr[i] !== elementToMove) { elements.push(arr[i]); } } if (position >= arr.length) { elements.push(elementToMove); } return elements; }, contains<T>(haystack: T[], needle: T): boolean { haystack = arrays.ensure(haystack); return haystack.indexOf(needle) !== -1; }, containsAny<T>(haystack: T[] | T, needles: T[] | T): boolean { let haystackArr = arrays.ensure(haystack); let needlesArr = arrays.ensure(needles); return needlesArr.some(element => { return haystackArr.indexOf(element) >= 0; }); }, containsAll<T>(haystack: T[] | T, needles: T[] | T): boolean { let haystackArr = arrays.ensure(haystack); let needlesArr = arrays.ensure(needles); return needlesArr.every(element => haystackArr.indexOf(element) >= 0); }, first<T>(arr: T[]): T { if (Array.isArray(arr)) { return arr[0]; } return arr; }, last<T>(arr: T[]): T { if (Array.isArray(arr)) { return arr[arr.length - 1]; } return arr; }, /** * @returns true if the given argument is an array and has a length > 0, false in any other case. */ hasElements<T>(arr: T[] | T): boolean { return !arrays.empty(arr); }, /** * @returns true if the given argument is not an array or the length of the array is 0, false in any other case. */ empty<T>(arr: T[] | T): boolean { if (Array.isArray(arr)) { return arr.length === 0; } return true; }, /** * @returns the size of the array, or 0 if the argument is not an array */ length<T>(arr: T[] | T): number { if (Array.isArray(arr)) { return arr.length; } return 0; }, pushAll<T>(arr: T[], arr2: T | T[]) { let a = arrays.ensure(arr2); arr.push(...a); }, /** * Merges the two given arrays and removes duplicate entries in O(n). * If the arrays contain objects instead of primitives, it uses their id to check for equality. */ union<T extends number | string | { id: string }>(array1: T[], array2: T[]): T[] { let result = []; let map = {}; array1 = arrays.ensure(array1); array2 = arrays.ensure(array2); array1.forEach(entry => { let key = entry as (number | string); if (typeof entry === 'object') { key = entry.id; } map[key] = entry; result.push(entry); }); array2.forEach(entry => { let key = entry as (number | string); if (typeof entry === 'object') { key = entry.id; } if (!(key in map)) { result.push(entry); } }); return result; }, equalsIgnoreOrder(arr: any[], arr2: any[]): boolean { // noinspection DuplicatedCode if (arr === arr2) { return true; } if ((!arr || arr.length === 0) && (!arr2 || arr2.length === 0)) { return true; } if (!arr || !arr2) { return false; } if (arr.length !== arr2.length) { return false; } return arrays.containsAll(arr, arr2); }, equals(arr: ArrayLike<any>, arr2: ArrayLike<any>): boolean { // noinspection DuplicatedCode if (arr === arr2) { return true; } if ((!arr || arr.length === 0) && (!arr2 || arr2.length === 0)) { return true; } if (!arr || !arr2) { return false; } if (arr.length !== arr2.length) { return false; } for (let i = 0; i < arr.length; i++) { if (arr[i] !== arr2[i]) { return false; } } return true; }, greater(arr: [], arr2: []): boolean { let arrLength = 0, arr2Length = 0; if (arr) { arrLength = arr.length; } if (arr2) { arr2Length = arr2.length; } return arrLength > arr2Length; }, eachSibling<T>(arr: ArrayLike<T>, element: T, func: (elem: T, index: number) => void) { if (!arr || !func) { return; } for (let i = 0; i < arr.length; i++) { let elementAtI = arr[i]; if (elementAtI !== element) { func(elementAtI, i); } } }, /** * Alternative implementation of Array.findIndex(callback [, thisArg]), which is supported by most browsers. * See Array.findIndex for a detailed description. * * @param optional "this" binding for predicate function */ findIndex<T>(arr: ArrayLike<T>, predicate: (elem: T, index: number, arr: T[]) => boolean, thisArg?: any): number { if (!arr || !predicate) { return -1; } for (let i = 0; i < arr.length; i++) { if (predicate.call(thisArg, arr[i], i, arr)) { return i; } } return -1; }, /** * * @param thisArg optional "this" binding for predicate function */ find<T>(arr: ArrayLike<T>, predicate: (elem: T, index: number, arr: T[]) => boolean, thisArg?: any): T { let index = arrays.findIndex(arr, predicate, thisArg); if (index === -1) { return null; } return arr[index]; }, findFrom<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean, reverse?: boolean): T { if (reverse) { return arrays.findFromReverse(arr, startIndex, predicate); } return arrays.findFromForward(arr, startIndex, predicate); }, findIndexFrom<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean, reverse?: boolean): number { if (reverse) { return arrays.findIndexFromReverse(arr, startIndex, predicate); } return arrays.findIndexFromForward(arr, startIndex, predicate); }, findFromForward<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean): T { let index = arrays.findIndexFromForward(arr, startIndex, predicate); if (index === -1) { return null; } return arr[index]; }, findIndexFromForward<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean): number { if (!arr || !predicate || startIndex >= arr.length) { return -1; } if (startIndex < 0) { startIndex = 0; } for (let i = startIndex; i < arr.length; i++) { let element = arr[i]; if (predicate(element, i)) { return i; } } return -1; }, findFromReverse<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean): T { let index = arrays.findIndexFromReverse(arr, startIndex, predicate); if (index === -1) { return null; } return arr[index]; }, findIndexFromReverse<T>(arr: ArrayLike<T>, startIndex: number, predicate: (elem: T, index: number) => boolean): number { if (!arr || !predicate || startIndex < 0) { return -1; } if (startIndex >= arr.length) { startIndex = arr.length - 1; } for (let i = startIndex; i >= 0; i--) { let element = arr[i]; if (predicate(element, i)) { return i; } } return -1; }, /** * Pushes all elements to the given array that are not null or undefined. */ pushIfDefined<T>(arr: T[], ...elements: T[]) { if (arr) { arrays.pushAll(arr, elements.filter(element => element !== null && element !== undefined)); } }, /** * Pushes the given element if it does not already exist in the array and the element is truthy. Thus, the array is like a Set where every element * can only be added once to the collection. Note: the comparison is done with the === operator. */ pushSet<T>(arr: T[], element: T) { if (element && arr.indexOf(element) === -1) { arr.push(element); } }, /** * Creates a string containing all elements in the array separated by the given delimiter. * @param encodeHtml true to encode the elements, false if not. Default is false */ format(arr: ArrayLike<string>, delimiter?: string, encodeHtml?: boolean): string { if (!arr || arr.length === 0) { return ''; } let output = ''; for (let i = 0; i < arr.length; i++) { let element = arr[i]; if (delimiter && i > 0 && i < arr.length) { output += delimiter; } if (encodeHtml) { element = strings.encode(element); } output += element; } return output; }, formatEncoded(arr: ArrayLike<string>, delimiter?: string): string { return arrays.format(arr, delimiter, true); }, max(arr: number[]): number { if (arr === null || arr === undefined) { return Math.max(arr as null | undefined); } // Math.max() returns 0 (not null!) if arr contains only null and negative elements. let filtered = arr.filter(objects.isNumber); return Math.max(...filtered); }, min(arr: number[]): number { if (arr === null || arr === undefined) { return Math.min(arr as null | undefined); } // Math.min() returns 0 (not null!) if arr contains only null and non-negative elements. let filtered = arr.filter(objects.isNumber); return Math.min(...filtered); }, /** * @returns all elements of the first array which are not in the second array */ diff<T>(arr1: T[], arr2: T[]): T[] { let diff = arr1.slice(); arrays.removeAll(diff, arr2); return diff; }, flatMap<T, R>(arr: T[] | T, func: (T) => R | R[] = (x => x)): R[] { let result = []; arrays.ensure(arr).forEach(element => arrays.pushAll(result, func(element))); return result; }, /** * Returns a flat array of all elements and their recursive child elements. * * @param arr The top-level list of all elements * @param childrenAccessor Function than extracts a list of child elements from a given element. Used to traverse the object structure. */ flattenRec<T>(arr: T[], childrenAccessor: (T) => T[]): T[] { return arrays.ensure(arr).reduce((acc, cur) => { acc.push(cur); if (cur && childrenAccessor) { acc = acc.concat(arrays.flattenRec(childrenAccessor(cur), childrenAccessor)); } return acc; }, []); }, /** * Replacement for indexOf() that works for arrays of jQuery objects (compares DOM nodes). * * @returns the first index of `$element` in `arr` or -1 if the element could not be found. */ $indexOf(arr: JQuery[], $element: JQuery): number { for (let i = 0; i < arr.length; i++) { if (arr[i][0] === $element[0]) { return i; } } return -1; }, /** * Replacement for remove() that works for arrays of jQuery objects (compares DOM nodes). * * @returns true if the element was removed, false otherwise. */ $remove(arr: JQuery[], $element: JQuery): boolean { let index = arrays.$indexOf(arr, $element); if (index >= 0) { arr.splice(index, 1); return true; } return false; }, randomElement<T>(array: T[]): T { if (!array) { return undefined; } if (!Array.isArray(array)) { return array; } if (!array.length) { return undefined; } return array[Math.floor(Math.random() * array.length)]; }, /** * Converts the given array to a map. For each element, key and value is determined by the given functions. * If no function is provided, the element itself is used. * * @param array array of elements * @param keyMapper function that maps each element to the target key * @param valueExtractor function that maps each element to the target value */ toMap<T>(array: T[], keyMapper: (el: T) => PropertyKey = (el => el + ''), valueMapper: (el: T) => any = (el => el)): any { return objects.createMap(arrays.ensure(array).reduce((map, element) => { map[keyMapper(element)] = valueMapper(element); return map; }, {})); }, /** * If the argument is an empty array, null is returned. Otherwise, the argument is returned unchanged. */ nullIfEmpty<T>(array: T[]): T[] { return arrays.empty(array) ? null : array; }, /** * Clears the content of an array <i>in-place</i>. All elements are removed from the array and the * length will be set to 0. If the given argument is not an array, nothing happens. * * This is a more readable version of `array.splice(0, a.length)`. * * The return value is an array of all deleted elements (never null). */ clear<T>(array: T[]): T[] { if (Array.isArray(array)) { return array.splice(0, array.length); } return []; }, /** * Swaps the two elements in the array. * Does nothing if array is null or undefined or one of the element is not part of the array. */ swap<T>(array: T[], element1: T, element2: T) { if (!array) { return; } let index1 = array.indexOf(element1); let index2 = array.indexOf(element2); if (index1 > -1 && index2 > -1) { array[index1] = element2; array[index2] = element1; } }, /** * Sorts the given array _in place_ according to the order of the template array. * Does nothing, if the array is null or undefined. * * @returns the given array which is now in order. */ sortBy<T>(array: T[], template: T[], direction: 'asc' | 'desc' = 'asc'): T[] { if (!array) { return []; } if (!template) { return array; } // Save the indices in a map to halve the execution time let map = new Map(); array = array.sort((element1, element2) => { let row1Index = objects.getOrSetIfAbsent(map, element1, element => template.indexOf(element)); let row2Index = objects.getOrSetIfAbsent(map, element2, row => template.indexOf(row)); if (direction === 'asc') { return row1Index - row2Index; } return row2Index - row1Index; }); return array; } };