scootloops
Version:
Advanced JavaScript iteration utilities providing powerful patterns for array operations, async flows, tree traversal, and performance optimization not available in native JavaScript.
459 lines (390 loc) • 13.2 kB
JavaScript
// Original functions with improvements
export function upLoop(start, end, callback) {
for (let i = start; i < end; i++) {
callback(i);
}
}
export function downLoop(start, end, callback) {
for (let i = start; i > end; i--) {
callback(i);
}
}
// Enhanced forEach that accepts a predicate function or value
export function forEach(array, matcher, callback) {
const predicate =
typeof matcher === 'function'
? matcher
: (element) => element === matcher;
array.forEach((element, index, arr) => {
if (predicate(element, index, arr)) {
// Only pass the element to match test expectations
callback(element);
}
});
}
export function mapIt(array, callback) {
return array.map(callback);
}
// Enhanced reduceIt to accept a custom reducer function
export function reduceIt(array, reducerOrInitial, initialValue) {
// Handle different parameter combinations
if (typeof reducerOrInitial === 'function') {
return array.reduce(reducerOrInitial, initialValue);
}
// Original behavior: sum with optional initial value
// If reducerOrInitial is undefined, use 0 as default
const initValue = reducerOrInitial === undefined ? 0 : reducerOrInitial;
return array.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initValue
);
}
// Fixed checkType function to correctly check specific types
function checkType(item, type) {
switch (type) {
case 'number':
return typeof item === 'number';
case 'string':
return typeof item === 'string';
case 'object':
return (
typeof item === 'object' &&
item !== null &&
!Array.isArray(item)
);
case 'boolean':
return typeof item === 'boolean';
case 'function':
return typeof item === 'function';
case 'undefined':
return typeof item === 'undefined';
case 'symbol':
return typeof item === 'symbol';
case 'bigint':
return typeof item === 'bigint';
default:
throw new TypeError('Invalid type specified');
}
}
export function filterIt(array, condition, value) {
if (!Array.isArray(array)) {
throw new Error(
'Invalid argument: The first argument must be an array.'
);
}
let operator;
if (condition.includes('.')) {
const conditionParts = condition.split('.');
operator = conditionParts[1];
} else {
operator = condition;
}
const operators = {
even: (item) => checkType(item, 'number') && item % 2 === 0,
odd: (item) => checkType(item, 'number') && item % 2 !== 0,
greaterThan: (item) => checkType(item, 'number') && item > value,
lessThan: (item) => checkType(item, 'number') && item < value,
startsWith: (item) =>
checkType(item, 'string') && item.startsWith(value),
endsWith: (item) => checkType(item, 'string') && item.endsWith(value),
exactMatch: (item) => item === value,
contains: (item) => checkType(item, 'string') && item.includes(value),
camelCase: (item) =>
checkType(item, 'string') && /^[a-z]+[A-Z][a-z]*$/.test(item),
isObject: (item) => checkType(item, 'object'),
isClass: (item) =>
typeof item === 'object' &&
item !== null &&
Object.getPrototypeOf(item) !== Object.prototype,
isArray: (item) => Array.isArray(item),
isNumber: (item) => checkType(item, 'number'),
isString: (item) => checkType(item, 'string'),
};
return array.filter((item) => {
const operation = operators[operator];
if (operation) {
return operation(item);
}
throw new Error(
'Invalid argument: The second argument must be a string in the format of "propName.operator" or an operator'
);
});
}
// NEW ADVANCED FUNCTIONS
/**
* Divides an array into batches/chunks of the specified size
* @param {Array} array - The array to divide into chunks
* @param {Number} size - The size of each chunk
* @returns {Array} An array of chunks, each being an array
*/
export function chunkIt(array, size) {
return Array.from({ length: Math.ceil(array.length / size) }, (_, index) =>
array.slice(index * size, (index + 1) * size)
);
}
/**
* Processes items in parallel using Promise.all, with concurrency limit
* @param {Array} items - The items to process
* @param {Function} asyncFn - Async function to apply to each item
* @param {Number} concurrency - Max number of concurrent operations
* @returns {Promise<Array>} Results of processing all items
*/
export async function parallelIt(
items,
asyncFn,
concurrency = Number.POSITIVE_INFINITY
) {
if (!concurrency || concurrency < 1) {
throw new Error('Concurrency must be a positive number');
}
if (concurrency === Number.POSITIVE_INFINITY) {
return Promise.all(items.map((item) => asyncFn(item)));
}
const chunks = chunkIt(items, concurrency);
const results = [];
for (const chunk of chunks) {
const chunkResults = await Promise.all(
chunk.map((item) => asyncFn(item))
);
results.push(...chunkResults);
}
return results;
}
/**
* Attempts to execute a function with retry logic
* @param {Function} fn - The function to execute
* @param {Object} options - Options for retry behavior
* @param {Number} options.retries - Number of retries (default: 3)
* @param {Number} options.delay - Base delay in ms (default: 300)
* @param {Boolean} options.exponential - Whether to use exponential backoff (default: true)
* @returns {Promise} Result of the function execution
*/
export async function retryIt(fn, options = {}) {
const { retries = 3, delay = 300, exponential = true } = options;
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
if (attempt < retries) {
const waitTime = exponential ? delay * 2 ** attempt : delay;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
}
throw lastError;
}
/**
* Iterates through nested objects/arrays and applies a callback to each value
* @param {Object|Array} obj - The object or array to iterate through
* @param {Function} callback - Function to call on each value (receives value, path, and original object)
* @param {String} [currentPath=''] - Internal parameter to track the current path
*/
export function deepIt(obj, callback, currentPath = '') {
if (obj === null || typeof obj !== 'object') {
callback(obj, currentPath, obj);
return;
}
for (const [key, value] of Object.entries(obj)) {
const path = currentPath ? `${currentPath}.${key}` : key;
callback(value, path, obj);
if (value !== null && typeof value === 'object') {
deepIt(value, callback, path);
}
}
}
/**
* Creates sliding windows of the specified size from an array
* @param {Array} array - The array to create windows from
* @param {Number} size - The size of each window
* @param {Number} [step=1] - The number of elements to slide the window by
* @returns {Array} An array of windows, each being an array
*/
export function windowIt(array, size, step = 1) {
if (size > array.length) {
return [];
}
const windows = [];
for (let i = 0; i <= array.length - size; i += step) {
windows.push(array.slice(i, i + size));
}
return windows;
}
/**
* Zips together multiple arrays into a single array of tuples
* @param {...Array} arrays - The arrays to zip together
* @returns {Array} An array of tuples, where each tuple contains elements from each input array
*/
export function zipIt(...arrays) {
const minLength = Math.min(...arrays.map((array) => array.length));
const result = [];
for (let i = 0; i < minLength; i++) {
result.push(arrays.map((array) => array[i]));
}
return result;
}
/**
* Creates a memoized version of a function
* @param {Function} fn - The function to memoize
* @param {Function} [keyFn] - Optional function to generate cache keys
* @returns {Function} Memoized function
*/
export function memoizeIt(fn, keyFn = (...args) => JSON.stringify(args)) {
const cache = new Map();
return function (...args) {
const key = keyFn(...args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
/**
* Traverses a tree structure using depth-first search (DFS)
* @param {Object} tree - The root of the tree
* @param {Function} callback - Function to call on each node
* @param {Object} options - Options for traversal
* @param {String} options.childrenKey - The property name for children (default: 'children')
*/
export function dfsIt(tree, callback, options = {}) {
const { childrenKey = 'children' } = options;
function traverse(node, depth = 0, path = []) {
if (!node) return;
callback(node, depth, path);
const children = node[childrenKey] || [];
children.forEach((child, index) => {
traverse(child, depth + 1, [...path, index]);
});
}
traverse(tree);
}
/**
* Traverses a tree structure using breadth-first search (BFS)
* @param {Object} tree - The root of the tree
* @param {Function} callback - Function to call on each node
* @param {Object} options - Options for traversal
* @param {String} options.childrenKey - The property name for children (default: 'children')
*/
export function bfsIt(tree, callback, options = {}) {
const { childrenKey = 'children' } = options;
if (!tree) return;
const queue = [{ node: tree, depth: 0, path: [] }];
while (queue.length > 0) {
const { node, depth, path } = queue.shift();
callback(node, depth, path);
const children = node[childrenKey] || [];
children.forEach((child, index) => {
queue.push({
node: child,
depth: depth + 1,
path: [...path, index],
});
});
}
}
/**
* Creates a throttled function that invokes at most once per specified interval
* @param {Function} fn - The function to throttle
* @param {Number} wait - The number of milliseconds to throttle invocations to
* @returns {Function} The throttled function
*/
export function throttleIt(fn, wait) {
let lastCall = 0;
let timeout = null;
let lastArgs = null;
return function (...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCall;
lastArgs = args;
if (timeSinceLastCall >= wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
lastCall = now;
return fn.apply(this, args);
}
if (!timeout) {
timeout = setTimeout(() => {
lastCall = Date.now();
timeout = null;
fn.apply(this, lastArgs);
}, wait - timeSinceLastCall);
}
};
}
/**
* Creates a debounced function that delays invoking until after wait milliseconds
* @param {Function} fn - The function to debounce
* @param {Number} wait - The number of milliseconds to delay
* @param {Boolean} immediate - Whether to invoke at the beginning of the wait period
* @returns {Function} The debounced function
*/
export function debounceIt(fn, wait, immediate = false) {
let timeout = null;
return function (...args) {
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if (!immediate) fn.apply(this, args);
}, wait);
if (callNow) fn.apply(this, args);
};
}
/**
* Creates a function that is the composition of the provided functions
* @param {...Function} fns - The functions to compose
* @returns {Function} A new function that is the composition of the input functions
*/
export function pipeIt(...fns) {
return (input) => fns.reduce((result, fn) => fn(result), input);
}
/**
* Async iterator with controlled concurrency
* @param {Array} items - The items to process
* @param {Function} asyncFn - Async function to apply to each item
* @param {Object} options - Options for the iterator
* @returns {Promise<Array>} Results of processing all items
*/
export async function asyncIterateIt(items, asyncFn, options = {}) {
const { concurrency = 1, delay = 0 } = options;
const results = [];
const pending = new Set();
async function processItem(item, index) {
try {
const result = await asyncFn(item, index);
results[index] = result;
} finally {
pending.delete(index);
}
}
for (let i = 0; i < items.length; i++) {
// Wait if we've reached max concurrency
while (pending.size >= concurrency) {
await Promise.race([
new Promise((resolve) => setTimeout(resolve, 10)),
...Array.from(pending).map((p) => Promise.resolve(p)),
]);
}
// Add to pending operations
pending.add(i);
// Process item (don't await here to allow concurrency)
const itemPromise = processItem(items[i], i);
// Add delay if specified
if (delay > 0 && i < items.length - 1) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Wait for all pending operations to complete
while (pending.size > 0) {
await Promise.race([
new Promise((resolve) => setTimeout(resolve, 10)),
...Array.from(pending).map((p) => Promise.resolve(p)),
]);
}
return results;
}