@eclipse-scout/core
Version:
Eclipse Scout runtime
723 lines (659 loc) • 20.9 kB
text/typescript
/*
* 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;
}
};