@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
601 lines (556 loc) • 14.7 kB
text/typescript
/**
* Functions for dealing with arrays.
* @module
*/
import { isSubclassOf } from "./class.ts";
import { random as rand } from "./number.ts";
import {
count as _count,
equals as _equals,
includeSlice as _includesSlice,
startsWith as _startsWith,
endsWith as _endsWith,
split as _split,
chunk as _chunk,
} from "./array/base.ts";
/**
* Returns the first element of the array, or `undefined` if the array is empty.
* This function is equivalent to `arr[0]` or `arr.at(0)`.
*/
export function first<T>(arr: T[]): T | undefined {
return arr[0];
}
/**
* Returns the last element of the array, or `undefined` if the array is empty.
* This function is equivalent to `arr[arr.length - 1]` or `arr.at(-1)`.
*/
export function last<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[arr.length - 1] : undefined;
}
/**
* Returns a random element of the array, or `undefined` if the array is empty.
* @param remove If `true`, the element will be removed from the array.
*
* @example
* ```ts
* import { random } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5];
*
* console.log(random(arr)); // 3 for example
*
* console.log(random(arr, true)); // 3 for example
* console.log(arr); // [1, 2, 4, 5]
* ```
*/
export function random<T>(arr: T[], remove = false): T | undefined {
if (!arr.length) {
return undefined;
} else if (arr.length === 1) {
if (remove) {
return arr.splice(0, 1)[0];
} else {
return arr[0];
}
}
const i = rand(0, arr.length - 1);
if (remove) {
return arr.splice(i, 1)[0];
} else {
return arr[i];
}
}
/**
* Counts the occurrence of the element in the array.
*
* @example
* ```ts
* import { count } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5, 2, 3, 4, 2];
*
* console.log(count(arr, 2)); // 3
* console.log(count(arr, 6)); // 0
* ```
*/
export function count<T>(arr: T[], item: T): number {
return _count(arr, item);
}
/**
* Performs a shallow compare to another array and see if it contains the same elements as
* this array.
*
* @example
* ```ts
* import { equals } from "@ayonli/jsext/array";
*
* const arr1 = [1, 2, 3, 4, 5];
* const arr2 = [{ foo: "bar" }];
*
* console.log(equals(arr1, [1, 2, 3, 4, 5])); // true
* console.log(equals(arr2, [{ foo: "bar" }])); // false, object refs are different
* ```
*/
export function equals<T>(arr1: T[], arr2: T[]): boolean {
return _equals(arr1, arr2);
}
/**
* Checks if the array contains another array as a slice of its contents.
*
* @example
* ```ts
* import { includesSlice } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5];
*
* console.log(includesSlice(arr, [2, 3, 4])); // true
* console.log(includesSlice(arr, [2, 4, 3])); // false
* ```
*/
export function includesSlice<T>(arr: T[], slice: T[]): boolean {
return _includesSlice(arr, slice);
}
/**
* Checks if the array starts with the given prefix.
*
* @example
* ```ts
* import { startsWith } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5];
*
* console.log(startsWith(arr, [1, 2])); // true
* console.log(startsWith(arr, [2, 1])); // false
* ```
*/
export function startsWith<T>(arr: T[], prefix: T[]): boolean {
return _startsWith(arr, prefix);
}
/**
* Checks if the array ends with the given suffix.
*
* @example
* ```ts
* import { endsWith } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5];
*
* console.log(endsWith(arr, [4, 5])); // true
* console.log(endsWith(arr, [5, 4])); // false
* ```
*/
export function endsWith<T>(arr: T[], suffix: T[]): boolean {
return _endsWith(arr, suffix);
}
/**
* Breaks the array into smaller chunks according to the given delimiter.
*
* @example
* ```ts
* import { split } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
*
* console.log(split(arr, 5)); // [[1, 2, 3, 4], [6, 7, 8, 9]]
* ```
*/
export function split<T>(arr: T[], delimiter: T): T[][] {
return _split(arr, delimiter) as T[][];
}
/**
* Breaks the array into smaller chunks according to the given length.
*
* @example
* ```ts
* import { chunk } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
*
* console.log(chunk(arr, 5)); // [[1, 2, 3, 4, 5], [6, 7, 8, 9]]
* ```
*/
export function chunk<T>(arr: T[], length: number): T[][] {
return _chunk(arr, length) as T[][];
}
/**
* Returns a subset of the array that contains only unique items.
*
* @example
* ```ts
* import { unique } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5, 2, 3, 4, 2];
*
* console.log(unique(arr)); // [1, 2, 3, 4, 5]
* ```
*/
export function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
/**
* @deprecated use `unique` instead.
*/
export const uniq = unique;
/**
* Returns a subset of the array that contains only unique items filtered by the
* given callback function.
*
* @example
* ```ts
* import { uniqueBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "foo" },
* { id: 4, name: "baz" },
* { id: 5, name: "bar" },
* ];
*
* console.log(uniqueBy(arr, item => item.name));
* // [
* // { id: 1, name: "foo" },
* // { id: 2, name: "bar" },
* // { id: 4, name: "baz" }
* // ]
* ```
*/
export function uniqueBy<T, K extends string | number | symbol>(
arr: T[],
fn: (item: T, i: number) => K
): T[] {
const map = new Map() as Map<K, T>;
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
const key = fn(item, i);
map.has(key) || map.set(key, item);
}
return [...map.values()];
}
/**
* @deprecated use `uniqueBy` instead.
*/
export const uniqBy = uniqueBy;
/**
* Reorganizes the elements in the array in random order.
*
* This function mutates the array.
*
* @example
* ```ts
* import { shuffle } from "@ayonli/jsext/array";
*
* const arr = [1, 2, 3, 4, 5];
*
* console.log(shuffle(arr)); // [3, 1, 5, 2, 4] for example
* console.log(arr); // [3, 1, 5, 2, 4]
* ```
*/
export function shuffle<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j] as T, arr[i] as T];
}
return arr;
}
/**
* Orders the items of the array according to the given callback function.
*
* @example
* ```ts
* import { orderBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "baz" },
* { id: 4, name: "qux" },
* ];
*
* console.log(orderBy(arr, item => item.name));
* // [
* // { id: 2, name: "bar" },
* // { id: 3, name: "baz" },
* // { id: 1, name: "foo" },
* // { id: 4, name: "qux" }
* // ]
*
* console.log(orderBy(arr, item => item.id, "desc"));
* // [
* // { id: 4, name: "qux" },
* // { id: 3, name: "baz" },
* // { id: 2, name: "bar" },
* // { id: 1, name: "foo" }
* // ]
* ```
*/
export function orderBy<T>(
arr: T[],
fn: (item: T, i: number) => string | number | bigint,
order?: "asc" | "desc"
): T[];
/**
* Orders the items of the array according to the specified comparable `key`
* (whose value must either be a numeric or string).
*
* @deprecated This signature is not in line with other functions, such as
* {@link groupBy} and {@link keyBy}, use the callback form instead.
*/
export function orderBy<T>(arr: T[], key: keyof T, order?: "asc" | "desc"): T[];
export function orderBy<T>(
arr: T[],
key: ((item: T, i: number) => string | number | bigint) | keyof T,
order: "asc" | "desc" = "asc"
): T[] {
const items = arr.slice();
if (typeof key === "function") {
return orderBy(items.map((item, i) => ({
key: key(item, i),
value: item,
})), "key", order).map(({ value }) => value);
}
items.sort((a, b) => {
if (typeof a !== "object" || typeof b !== "object" ||
!a || !b ||
Array.isArray(a) || Array.isArray(b)
) {
return -1;
}
const _a = a[key];
const _b = b[key];
if (_a === undefined || _b === undefined) {
return -1;
}
if (typeof _a === "number" && typeof _b === "number") {
return _a - _b;
} else if ((typeof _a === "string" && typeof _b === "string")
|| (typeof _a === "bigint" && typeof _b === "bigint")
) {
if (_a < _b) {
return -1;
} else if (_a > _b) {
return 1;
} else {
return 1;
}
} else {
return -1;
}
});
if (order === "desc") {
items.reverse();
}
return items;
};
/**
* Groups the items of the array according to the comparable values returned by a provided
* callback function.
*
* The returned record / map has separate properties for each group, containing arrays with
* the items in the group.
*
* @example
* ```ts
* import { groupBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "foo" },
* { id: 4, name: "baz" },
* { id: 5, name: "bar" },
* ];
*
* console.log(groupBy(arr, item => item.name));
* // {
* // foo: [
* // { id: 1, name: "foo" },
* // { id: 3, name: "foo" }
* // ],
* // bar: [
* // { id: 2, name: "bar" },
* // { id: 5, name: "bar" }
* // ],
* // baz: [
* // { id: 4, name: "baz" }
* // ]
* // }
* ```
*/
export function groupBy<T, K extends string | number | symbol>(
arr: T[],
fn: (item: T, i: number) => K,
type?: ObjectConstructor
): Record<K, T[]>;
/**
* @example
* ```ts
* import { groupBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "foo" },
* { id: 4, name: "baz" },
* { id: 5, name: "bar" },
* ];
*
* console.log(groupBy(arr, item => item.name, Map));
* // Map {
* // "foo" => [
* // { id: 1, name: "foo" },
* // { id: 3, name: "foo" }
* // ],
* // "bar" => [
* // { id: 2, name: "bar" },
* // { id: 5, name: "bar" }
* // ],
* // "baz" => [
* // { id: 4, name: "baz" }
* // ]
* // }
* ```
*/
export function groupBy<T, K>(
arr: T[],
fn: (item: T, i: number) => K,
type: MapConstructor
): Map<K, T[]>;
export function groupBy<T, K>(
arr: T[],
fn: (item: T, i: number) => K,
type: ObjectConstructor | MapConstructor = Object
): any {
if (type === Map || isSubclassOf(type, Map)) {
const groups = new type() as Map<any, T[]>;
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
const key = fn(item, i);
const list = groups.get(key);
if (list) {
list.push(item);
} else {
groups.set(key, [item]);
}
}
return groups;
} else {
const groups: Record<string | number | symbol, T[]> = {};
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
const key = fn(item, i) as string | number | symbol;
const list = groups[key];
if (list) {
list.push(item);
} else {
groups[key] = [item];
}
}
return groups;
}
};
/**
* Creates a record or map from the items of the array according to the comparable values
* returned by a provided callback function.
*
* This function is similar to {@link groupBy}, except it overrides values if the same
* property already exists instead of grouping them as a list.
*
* @example
* ```ts
* import { keyBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "baz" },
* ];
*
* console.log(keyBy(arr, item => item.name));
* // {
* // foo: { id: 1, name: "foo" },
* // bar: { id: 2, name: "bar" },
* // baz: { id: 3, name: "baz" }
* // }
* ```
*/
export function keyBy<T, K extends string | number | symbol>(
arr: T[],
fn: (item: T, i: number) => K,
type?: ObjectConstructor
): Record<K, T>;
/**
* @example
* ```ts
* import { keyBy } from "@ayonli/jsext/array";
*
* const arr = [
* { id: 1, name: "foo" },
* { id: 2, name: "bar" },
* { id: 3, name: "baz" },
* ];
*
* console.log(keyBy(arr, item => item.name, Map));
* // Map {
* // "foo" => { id: 1, name: "foo" },
* // "bar" => { id: 2, name: "bar" },
* // "baz" => { id: 3, name: "baz" }
* // }
* ```
*/
export function keyBy<T, K>(
arr: T[],
fn: (item: T, i: number) => K,
type: MapConstructor
): Map<K, T>;
export function keyBy<T, K>(
arr: T[],
fn: (item: T, i: number) => K,
type: ObjectConstructor | MapConstructor = Object
): Record<string | number | symbol, T> | Map<K, T> {
if (type === Map || isSubclassOf(type, Map)) {
const map = new type() as Map<any, T>;
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
const key = fn(item, i);
map.set(key, item);
}
return map;
} else {
const record = {} as Record<string | number | symbol, T>;
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
const key = fn(item, i) as string | number | symbol;
record[key] = item;
}
return record;
}
}
/**
* Returns a tuple of two arrays with the first one containing all elements in
* the given array that match the given predicate and the second one containing
* all that do not.
*
* @example
* ```ts
* import { partition } from "@ayonli/jsext/array";
*
* const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
* const [even, odd] = partition(arr, item => item % 2 === 0);
*
* console.log(even); // [0, 2, 4, 6, 8]
* console.log(odd); // [1, 3, 5, 7, 9]
* ```
*/
export function partition<T>(
arr: T[],
predicate: (item: T, i: number) => boolean
): [T[], T[]] {
const match: T[] = [];
const rest: T[] = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as T;
(predicate(item, i) ? match : rest).push(item);
}
return [match, rest];
}