342 lines
9.7 KiB
TypeScript
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;
|
|
}
|