tom-weatherhead/common-utilities.ts

View on GitHub
src/arrays.ts

Summary

Maintainability
B
5 hrs
Test Coverage
// github:tom-weatherhead/common-utilities.ts/src/arrays.ts

// For an excellent resource on the JavaScript Array API, see:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array

import { getRandomNonNegativeInteger, sum } from './numbers';

/**
 * @method cloneArray
 * @param  {T[]}   array            An array
 * @return {T[]}                    A shallow copy of the array that was passed in
 */
export function cloneArray<T>(array: T[]): T[] {
    return array.slice(0); // See https://davidwalsh.name/javascript-clone-array
    // TODO: Try: return array.slice();
}

export function doesConsecutiveElementsConditionHold<T>(
    array: T[],
    fn: (element1: T, element2: T) => boolean,
    defaultResult = true
): boolean {
    if (array.length <= 1) {
        // The array is too short to have any consecutive elements.

        return defaultResult;
    }

    for (let i = 0; i < array.length - 1; i++) {
        if (!fn(array[i], array[i + 1])) {
            return false;
        }
    }

    return true;
}

export function isArrayInIncreasingOrder(array: number[]): boolean {
    return doesConsecutiveElementsConditionHold(array, (x, y) => x < y, true);
}

export function isArrayInNonDecreasingOrder(array: number[]): boolean {
    return doesConsecutiveElementsConditionHold(array, (x, y) => x <= y, true);
}

export function isArrayInDecreasingOrder(array: number[]): boolean {
    return doesConsecutiveElementsConditionHold(array, (x, y) => x > y, true);
}

export function isArrayInNonIncreasingOrder(array: number[]): boolean {
    return doesConsecutiveElementsConditionHold(array, (x, y) => x >= y, true);
}

export function findSuperlativeElement<T>(array: T[], fn: (x: T, y: T) => T): T {
    if (!array.length) {
        // From https://www.sitepen.com/blog/typescript-2-3-the-sexy-default-type-argument/ :
        // return <T> {}; // Return the default value for type T.

        throw new Error('findSuperlativeElement() : array has a length of zero.');
    }

    return array.slice(1).reduce(fn, array[0]);
}

export function max<T>(array: T[]): T {
    return findSuperlativeElement(array, (x, y) => (x > y ? x : y));
}

export function min<T>(array: T[]): T {
    return findSuperlativeElement(array, (x, y) => (x < y ? x : y));
}

// NO: Array.includes() exists in Typescript, so we don't need to implement arrayIncludes()
// Note: error TS2339: Property 'includes' does not exist on type 'number[]'.
export function arrayIncludes<T>(array: T[], element: T): boolean {
    return array.indexOf(element) >= 0;
}

export function appendIfNotFound<T>(array: T[], ...elements: T[]): T[] {
    for (const element of elements) {
        if (!arrayIncludes(array, element)) {
            array.push(element);
        }
    }

    return array;
}

export function removeDuplicatesFromArray<T>(array: T[]): T[] {
    // See the discussion at https://gist.github.com/telekosmos/3b62a31a5c43f40849bb

    // JavaScript Set: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set

    // See https://stackoverflow.com/questions/13486479/how-to-get-an-array-of-unique-values-from-an-array-containing-duplicates-in-java
    // Discussion about performace: See https://medium.com/@jakubsynowiec/unique-array-values-in-javascript-7c932682766c
    // return [...new Set(array)]; // Yes. Requires ES6, since it uses the "spread" operator ("...").

    // return Array.from(new Set(array)); // Yes

    return array.reduce(
        (x: T[], y: T) => (x.includes(y) ? x : [...x, y]), // Array.includes() exists in Typescript
        // (x: T[], y: T) => x.indexOf(y) >= 0 ? x : [...x, y],
        []
    ); // Yes. From svnpenn.

    // ThAW 2021-10-29 : return appendIfNotFound([], ...array);
}

export function flattenOneLevel<T>(a: Array<T[] | T>, b: T[] = []): T[] {
    return a.reduce((accumulator: T[], element: T[] | T) => {
        const castedElement: T[] = element as T[];

        // if (isArray(element)) {
        // TODO?: if (typeof castedElement === 'Array') {
        if (typeof castedElement !== 'undefined') {
            return accumulator.concat(castedElement);
        } else {
            accumulator.push(element as T);

            return accumulator;
        }
    }, b);
}

export function flattenAllLevels<T>(a: unknown[], b: T[] = []): T[] {
    return a.reduce((accumulator: T[], element: unknown) => {
        const elementAsArray = element as unknown[];
        const elementAsT = element as T;

        if (typeof elementAsArray !== 'undefined') {
            flattenAllLevels(elementAsArray, accumulator);
        } else if (typeof elementAsT !== 'undefined') {
            accumulator.push(elementAsT);
        }

        return accumulator;
    }, b);
}

export function getRandomArrayElement<T>(array: T[]): T | undefined {
    if (!array.length) {
        return undefined;
    }

    return array[getRandomNonNegativeInteger(array.length)];
}

export function propertySum(array: Record<string, unknown>[], propertyName: string): number {
    return sum(
        array.map((element: Record<string, unknown>) => element[propertyName] as number)
    );
}

// Categorize? Or pigeonhole?

// error TS2538: Type 'symbol' cannot be used as an index type.
// type ObjectKeyType = number | string | symbol;
type ObjectKeyType = number | string;

// TODO: Use this:
// export function categorizeArrayElementsByFunction<T, U>(
//     array: T[],
//     fn: (element: T) => U // (element: T) => ObjectKeyType
// ): Record<U, T[]> { ... }

// T must be an ObjectValueType
// export function categorizeArrayElementsByFunction<T>(
//     array: T[],
//     fn: (element: T) => string // (element: T) => ObjectKeyType
// ): Record<string, unknown> {

export function categorizeArrayElementsByFunction<T>(
    array: T[],
    fn: (element: T) => ObjectKeyType // (element: T) => ObjectKeyType
): Record<ObjectKeyType, T[]> {
    // [...new Set(array)] : Remove duplicate elements
    // const propertyValues = [...new Set(array.map(element => fn(element)))];
    const propertyValues = removeDuplicatesFromArray(array.map((element) => fn(element)));
    const start: Record<ObjectKeyType, T[]> = {};
    // Or: const start = new Map<U, T[]>();

    propertyValues.sort();

    return propertyValues.reduce((accumulator, propertyValue) => {
        accumulator[propertyValue] = array.filter((element) => fn(element) === propertyValue);

        return accumulator;
    }, start);
}

// export function categorizeArrayElementsByFunction_version2(array, fn) {

//     if (!isArray(array)) {
//         return undefined;
//     }

//     return array.reduce(
//         (accumulator, element) => {
//             const key = fn(element);

//             if (!accumulator[key]) {
//                 accumulator[key] = [];
//             }

//             // Array.push() returns the length of the array after the push.
//             accumulator[key].push(element);

//             return accumulator;
//         },
//         {}
//     );
// }

type Foo1ElementType = Record<ObjectKeyType, ObjectKeyType>;

export function categorizeArrayElementsByProperty(
    array: Foo1ElementType[],
    propertyName: ObjectKeyType
): Record<ObjectKeyType, Foo1ElementType[]> {
    return categorizeArrayElementsByFunction(
        array,
        (element: Foo1ElementType) => element[propertyName]
    );
}

export function getLastElementOfArray<T>(array: T[]): T | undefined {
    if (!array.length) {
        return undefined;
    }

    // From https://solidfoundationwebdev.com/blog/posts/3-methods-to-get-the-last-element-of-an-array-in-javascript :

    // return array[array.length - 1];

    // return array.pop(); // But this will remove the last element from the array.

    return array.slice(-1)[0];
}

// createAndFillArray(obj, d1, d2, d3, ... dn) :
// Create an n-dimensional array of size d1 x d2 x ... x dn, with all elements set to obj

// NO: Try this recursive type definition: type Foo<T> = T | Foo<T[]>
// export function createAndFillArray<T>(obj: T, ...dimensions: number[]): Foo<T> { ...

// error TS2456: Type alias 'Metaarray' circularly references itself.
// type Metaarray<T> = T | Metaarray<T[]>;

// export function createAndFillArray<T>(
//     obj: T,
//     ...dimensions: number[]
// ): Metaarray<T> { ... }

export function createAndFillArray(obj: unknown, ...dimensions: number[]): unknown {
    if (!dimensions || !dimensions.length) {
        return obj;
    }

    const result = [];
    const d1 = dimensions.shift() as number;

    for (let i = 0; i < d1; i++) {
        result.push(createAndFillArray(obj, ...dimensions));
    }

    dimensions.unshift(d1);

    return result;
}

export function createArrayFromElement<T>(element: T, length: number): T[] {
    return new Array(Math.max(length, 0)).fill(element);
}

export function generateAllSubsets<T>(array: T[], i = 0, accumulator: T[] = []): T[][] {
    if (i >= array.length) {
        return [accumulator];
    }

    const list1 = generateAllSubsets(array, i + 1, accumulator);
    const list2 = generateAllSubsets(array, i + 1, accumulator.concat([array[i]]));

    return list1.concat(list2);
}

export function padOrTrimArrayAtStart<T>(
    array: T[],
    desiredLength: number,
    defaultElement: T
): T[] {
    const diff = array.length - desiredLength;

    if (diff >= 0) {
        return array.slice(diff);
    } else {
        return new Array(-diff).fill(defaultElement).concat(array);
    }
}

export function selectElementsByIndex<T>(array: T[], ...selectedIndices: number[]): T[] {
    return selectedIndices
        .filter((value: number) => value < array.length)
        .map((selectedIndex: number) => array[selectedIndex]);
}

export function selectElementsOrDefaultByIndex<T>(
    array: T[],
    defaultValue: T,
    ...selectedIndices: number[]
): T[] {
    return selectedIndices.map((selectedIndex: number) =>
        selectedIndex < array.length ? array[selectedIndex] : defaultValue
    );
}

// transpose2d changes [[A1, A2, A3], [B1, B2, B3], [C1, C2, C3]]
// to: [[A1, B1, C1], [A2, B2, C2], [A3, B3, C3]]

export function transpose2d<T>(matrix: T[][]): T[][] {
    const minLength = Math.min(...matrix.map((row) => row.length));
    const result: T[][] = [];

    for (let i = 0; i < minLength; i++) {
        const row: T[] = [];

        for (const array of matrix) {
            row.push(array[i]);
        }

        result.push(row);
    }

    return result;
}

// export function cascade1d1array<T>(
//     operation: (s: T, element: T) => T, // Function,
//     seedValue: T,
//     array: T[]
// ): T[] {
//     return array.reduce((accumulator: T[], element: T) => {
//         seedValue = operation(seedValue, element);
//         accumulator.push(seedValue);

//         return accumulator;
//     }, []);
// }

// export function all(array: boolean[]): boolean {
export function all(array: Iterable<boolean>): boolean {
    for (const value of array) {
        if (!value) {
            return false;
        }
    }

    return true;
}

// export function any(array: boolean[]): boolean {
export function any(array: Iterable<boolean>): boolean {
    for (const value of array) {
        if (value) {
            return true;
        }
    }

    return false;
}

// export function none(array: boolean[]): boolean {
export function none(array: Iterable<boolean>): boolean {
    for (const value of array) {
        if (value) {
            return false;
        }
    }

    return true;
}

// export function notAll(array: boolean[]): boolean {
export function notAll(array: Iterable<boolean>): boolean {
    for (const value of array) {
        if (!value) {
            return true;
        }
    }

    return false;
}

export const some = any;

export function mapLastElementOfArray<T, U>(array: T[], fn: (element: T) => U, dflt: U): U {
    if (array.length === 0) {
        return dflt;
    }

    const lastElement = array[array.length - 1];

    return fn(lastElement);
}

// Return a randomly reordered version of the array.

export function shuffle<T>(array: T[]): T[] {
    const arrayCopy = array.slice(0);
    const result: T[] = [];

    while (arrayCopy.length > 0) {
        const i = getRandomNonNegativeInteger(arrayCopy.length);
        const arrayOfDeletedElements = arrayCopy.splice(i, 1);

        result.push(arrayOfDeletedElements[0]);
    }

    return result;
}