kepler-chat/src/lib/utils/array.ts
2025-06-19 06:01:59 -07:00

342 lines
9.7 KiB
TypeScript

/*
Installed from @ieedan/std
*/
/** Maps the provided map into an array using the provided mapping function.
*
* @param map Map to be entered into an array
* @param fn A mapping function to transform each pair into an item
* @returns
*
* ## Usage
* ```ts
* console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 }
*
* const arr = fromMap(map, (_, value) => value);
*
* console.log(arr); // [5, 4, 3, 2, 1]
* ```
*/
export function fromMap<K, V, T>(map: Map<K, V>, fn: (key: K, value: V) => T): T[] {
const items: T[] = [];
for (const [key, value] of map) {
items.push(fn(key, value));
}
return items;
}
export function fromRecord<V, T>(map: Record<string, V>, fn: (key: string, value: V) => T): T[] {
const items: T[] = [];
for (const [key, value] of Object.entries(map)) {
items.push(fn(key, value));
}
return items;
}
/** Calculates the sum of all elements in the array based on the provided function.
*
* @param arr Array of items to be summed.
* @param fn Summing function
* @returns
*
* ## Usage
*
* ```ts
* const total = sum([1, 2, 3, 4, 5], (num) => num);
*
* console.log(total); // 15
* ```
*/
export function sum<T>(arr: T[], fn: (item: T) => number): number {
let total = 0;
for (const item of arr) {
total = total + fn(item);
}
return total;
}
/** Maps the provided array into a record
*
* @param arr Array of items to be entered into a record
* @param fn A mapping function to transform each item into a key value pair
* @returns
*
* ## Usage
* ```ts
* const record = toRecord([5, 4, 3, 2, 1], (item, i) => [i, item]);
*
* console.log(record); // { "0": 5, "1": 4, "2": 3, "3": 2, "4": 1 }
* ```
*/
export function toRecord<T, V>(
arr: T[],
fn: (item: T, index: number) => [key: string, value: V]
): Record<string, V> {
const record: Record<string, V> = {};
for (let i = 0; i < arr.length; i++) {
const [key, value] = fn(arr[i]!, i);
record[key] = value;
}
return record;
}
/** Maps the provided array into a map
*
* @param arr Array of items to be entered into a map
* @param fn A mapping function to transform each item into a key value pair
* @returns
*
* ## Usage
* ```ts
* const map = toMap([5, 4, 3, 2, 1], (item, i) => [i, item]);
*
* console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 }
* ```
*/
export function toMap<T, K, V>(
arr: T[],
fn: (item: T, index: number) => [key: K, value: V]
): Map<K, V> {
const map: Map<K, V> = new Map();
for (let i = 0; i < arr.length; i++) {
const [key, value] = fn(arr[i]!, i);
map.set(key, value);
}
return map;
}
type IterateReturn<T> = [
T,
{
isFirst: boolean;
isLast: boolean;
array: T[];
index: number;
length: number;
},
];
/**
* Returns an an iterator that iterates over the given array.
* Each returned item contains helpful properties, such as
* `isFirst`, `isLast`, `array`, `index`, and `length`
*
* @param array The array to iterate over.
* @returns An iterator that iterates over the given array.
*/
export function* iterate<T>(array: T[]): Generator<IterateReturn<T>> {
for (let i = 0; i < array.length; i++) {
yield [
array[i]!,
{
isFirst: i === 0,
isLast: i === array.length - 1,
array,
index: i,
length: array.length,
},
];
}
}
export function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
/**
* Defines the possible directions for movement within the matrix.
*/
export type Direction = 'up' | 'down' | 'left' | 'right';
/**
* Options for the getNextMatrixItem function.
* @template T The type of items stored in the matrix.
*/
export interface GetNextMatrixItemOptions<T> {
/** The matrix (an array of arrays) where rows can have varying lengths. */
matrix: T[][];
/** The 0-based index of the current row. */
currentRow: number;
/** The 0-based index of the current column. */
currentCol: number;
/** The direction to move ('up', 'down', 'left', 'right'). */
direction: Direction;
/**
* An optional predicate function that determines if an item is "available".
* If an item is not available, the function will attempt to find the next available candidate.
* Defaults to always returning true (all items are available).
* @param item The item to check.
* @returns True if the item is available, false otherwise.
*/
isAvailable?: (item: T) => boolean;
}
/**
* Retrieves the next item in a matrix based on a current position and a direction.
* This function is designed to work with "jagged" arrays (rows can have different lengths).
*
* Special behavior for 'up'/'down': If the target row is shorter than the current column,
* the column index will snap to the rightmost valid column of the target row.
*
* If an `isAvailable` function is provided and the initially calculated next item is
* not available, the function will attempt to find the next available candidate
* within the target row:
* - For 'up'/'down': It will scan left from the snapped column.
* - For 'left': It will continue scanning left.
* - For 'right': It will continue scanning right.
*
* @template T The type of items stored in the matrix.
* @param options The options object containing matrix, current position, direction, and optional isAvailable predicate.
* @returns The item at the next valid and available position, or `undefined` if no such item is found.
*/
export function getNextMatrixItem<T>(options: GetNextMatrixItemOptions<T>): T | undefined {
const { matrix, currentRow, currentCol, direction, isAvailable = (_i) => true } = options;
// --- 1. Input Validation: Matrix and Current Position ---
if (!matrix || !Array.isArray(matrix) || matrix.length === 0) {
return undefined;
}
if (currentRow < 0 || currentRow >= matrix.length) {
return undefined;
}
const currentRowArray = matrix[currentRow];
if (
!currentRowArray ||
!Array.isArray(currentRowArray) ||
currentCol < 0 ||
currentCol >= currentRowArray.length
) {
return undefined;
}
// --- 2. Calculate Tentative Next Coordinates ---
let nextRow = currentRow;
let nextCol = currentCol;
switch (direction) {
case 'up':
nextRow--;
break;
case 'down':
nextRow++;
break;
case 'left':
nextCol--;
break;
case 'right':
nextCol++;
break;
}
// --- 3. Validate and Adjust Next Coordinates Against Matrix Bounds ---
// For vertical movements, skip empty rows and rows where all items are unavailable
if (direction === 'up' || direction === 'down') {
while (nextRow >= 0 && nextRow < matrix.length) {
const nextRowArray = matrix[nextRow];
if (nextRowArray && Array.isArray(nextRowArray) && nextRowArray.length > 0) {
// Check if there's at least one available item in this row
const hasAvailableItem = nextRowArray.some(
(item) => item !== undefined && item !== null && isAvailable(item)
);
if (hasAvailableItem) {
break; // Found a row with at least one available item
}
}
// Skip empty row or row with all unavailable items
nextRow += direction === 'down' ? 1 : -1;
}
if (nextRow < 0 || nextRow >= matrix.length) {
return undefined; // Out of vertical bounds after skipping empty/unavailable rows
}
}
// For horizontal movements, validate row bounds and get the row array
if (direction === 'left' || direction === 'right') {
if (nextRow < 0 || nextRow >= matrix.length) {
return undefined; // Out of vertical bounds
}
}
const nextRowArray = matrix[nextRow];
if (!nextRowArray || !Array.isArray(nextRowArray)) {
return undefined; // The row itself is malformed or non-existent
}
// For horizontal movements, check if the row is empty
if ((direction === 'left' || direction === 'right') && nextRowArray.length === 0) {
return undefined; // Can't move horizontally in an empty row
}
// --- NEW LOGIC: Adjust nextCol for vertical movements if it's out of bounds ---
if (direction === 'up' || direction === 'down') {
// Clamp nextCol to the last valid index of the target row if it's too far right
nextCol = Math.min(nextCol, nextRowArray.length - 1);
}
// --- 4. Find the Next Available Item ---
// For horizontal movements, skip empty columns by finding next valid position
if (direction === 'left' || direction === 'right') {
let candidateCol = nextCol;
const increment = direction === 'right' ? 1 : -1;
while (candidateCol >= 0 && candidateCol < nextRowArray.length) {
const candidateItem = nextRowArray[candidateCol];
if (candidateItem !== undefined && candidateItem !== null && isAvailable(candidateItem)) {
return candidateItem;
}
candidateCol += increment;
}
return undefined; // No available item found in the horizontal direction
}
// Initial check for bounds after clamping/calculation
if (nextCol < 0 || nextCol >= nextRowArray.length) {
return undefined; // No valid column to start searching from in the target row
}
// Loop to find the next available item for vertical movements
let candidateCol = nextCol;
if (direction === 'up' || direction === 'down') {
// For vertical moves, try the calculated/clamped 'candidateCol', then scan left, then scan right
while (candidateCol >= 0) {
const candidateItem = nextRowArray[candidateCol];
if (candidateItem !== undefined && candidateItem !== null && isAvailable(candidateItem)) {
return candidateItem;
}
candidateCol--; // Move left to find an available item
}
// If not found scanning left, try scanning right from the original position
candidateCol = nextCol + 1;
while (candidateCol < nextRowArray.length) {
const candidateItem = nextRowArray[candidateCol];
if (candidateItem !== undefined && candidateItem !== null && isAvailable(candidateItem)) {
return candidateItem;
}
candidateCol++; // Move right to find an available item
}
}
// If no available item was found in the search path
return undefined;
}