/** * Data transformation utilities */ import { GroupByResult, AggregationFunction, AggregationResult } from './types'; /** * Get nested property value from object * @param obj - Object to get value from * @param path - Property path (supports dot notation) */ function getNestedValue(obj: any, path: string | keyof any): any { if (typeof path === 'string' && path.includes('.')) { return path.split('.').reduce((current, key) => current?.[key], obj); } return obj?.[path]; } /** * Group array of objects by a specific property * @param data - Array to group * @param key - Property to group by */ export function groupBy(data: T[], key: keyof T): GroupByResult[] { if (!Array.isArray(data) || data.length === 0) return []; const groups = new Map(); data.forEach(item => { const groupKey = getNestedValue(item, key); if (!groups.has(groupKey)) { groups.set(groupKey, []); } groups.get(groupKey)!.push(item); }); return Array.from(groups.entries()).map(([groupKey, items]) => ({ key: groupKey, items, count: items.length })); } /** * Group array by multiple properties * @param data - Array to group * @param keys - Properties to group by */ export function groupByMultiple(data: T[], keys: (keyof T)[]): GroupByResult[] { if (!Array.isArray(data) || data.length === 0) return []; if (!keys || keys.length === 0) return [{ key: null, items: data, count: data.length }]; const groups = new Map(); data.forEach(item => { const groupKey = keys.map(key => String(getNestedValue(item, key))).join('|'); if (!groups.has(groupKey)) { groups.set(groupKey, []); } groups.get(groupKey)!.push(item); }); return Array.from(groups.entries()).map(([groupKey, items]) => ({ key: groupKey, items, count: items.length })); } /** * Aggregate values using specified functions * @param data - Array to aggregate * @param key - Property to aggregate * @param functions - Aggregation functions to apply */ export function aggregate( data: T[], key: keyof T, functions: AggregationFunction[] ): AggregationResult { if (!Array.isArray(data) || data.length === 0) { return functions.reduce((acc, fn) => ({ ...acc, [fn]: 0 }), {}); } const values = data .map(item => getNestedValue(item, key)) .filter(value => value != null && !isNaN(Number(value))) .map(value => Number(value)); if (values.length === 0) { return functions.reduce((acc, fn) => ({ ...acc, [fn]: 0 }), {}); } const result: AggregationResult = {}; functions.forEach(fn => { switch (fn) { case 'sum': result['sum'] = values.reduce((acc, val) => acc + val, 0); break; case 'avg': result['avg'] = values.reduce((acc, val) => acc + val, 0) / values.length; break; case 'min': result['min'] = Math.min(...values); break; case 'max': result['max'] = Math.max(...values); break; case 'count': result['count'] = values.length; break; } }); return result; } /** * Group by and aggregate in one operation * @param data - Array to process * @param groupKey - Property to group by * @param aggregateKey - Property to aggregate * @param functions - Aggregation functions */ export function groupAndAggregate( data: T[], groupKey: keyof T, aggregateKey: keyof T, functions: AggregationFunction[] ): Array<{ group: any; aggregates: AggregationResult }> { const groups = groupBy(data, groupKey); return groups.map(group => ({ group: group.key, aggregates: aggregate(group.items, aggregateKey, functions) })); } /** * Extract specific properties from objects (pluck) * @param data - Array of objects * @param keys - Properties to extract */ export function pluck, K extends keyof T>(data: T[], keys: K[]): Pick[] { if (!Array.isArray(data) || data.length === 0) return []; if (!keys || keys.length === 0) return []; return data.map(item => { const result = {} as Pick; keys.forEach(key => { if (key in item) { result[key] = item[key]; } }); return result; }); } /** * Extract single property values into array * @param data - Array of objects * @param key - Property to extract */ export function pluckProperty(data: T[], key: K): T[K][] { if (!Array.isArray(data) || data.length === 0) return []; return data.map(item => getNestedValue(item, key)).filter(value => value !== undefined); } /** * Flatten nested arrays * @param data - Array that may contain nested arrays * @param depth - Maximum depth to flatten (default: 1) */ export function flatten(data: any[], depth: number = 1): T[] { if (!Array.isArray(data)) return []; const flattenRecursive = (arr: any[], currentDepth: number): any[] => { return currentDepth > 0 ? arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenRecursive(val, currentDepth - 1)) : acc.concat(val), []) : arr.slice(); }; return flattenRecursive(data, depth); } /** * Flatten array completely (all levels) * @param data - Array to flatten */ export function flattenDeep(data: any[]): T[] { if (!Array.isArray(data)) return []; return data.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []); } /** * Create pivot table from data * @param data - Array to pivot * @param rowKey - Property for rows * @param colKey - Property for columns * @param valueKey - Property for values * @param aggregateFunction - How to aggregate values */ export function pivot( data: T[], rowKey: keyof T, colKey: keyof T, valueKey: keyof T, aggregateFunction: AggregationFunction = 'sum' ): { [row: string]: { [col: string]: number } } { if (!Array.isArray(data) || data.length === 0) return {}; const result: { [row: string]: { [col: string]: number } } = {}; data.forEach(item => { const row = String(getNestedValue(item, rowKey)); const col = String(getNestedValue(item, colKey)); const value = Number(getNestedValue(item, valueKey)) || 0; if (!result[row]) { result[row] = {}; } if (!result[row][col]) { result[row][col] = 0; } switch (aggregateFunction) { case 'sum': result[row][col] += value; break; case 'count': result[row][col] += 1; break; case 'avg': // For avg, we need to track sum and count separately // This is a simplified version that treats each occurrence as separate result[row][col] = (result[row][col] + value) / 2; break; case 'min': result[row][col] = Math.min(result[row][col], value); break; case 'max': result[row][col] = Math.max(result[row][col], value); break; } }); return result; } /** * Transpose 2D array (flip rows and columns) * @param data - 2D array to transpose */ export function transpose(data: T[][]): T[][] { if (!Array.isArray(data) || data.length === 0) return []; if (!Array.isArray(data[0])) return []; const rows = data.length; const cols = data[0].length; const result: T[][] = []; for (let col = 0; col < cols; col++) { result[col] = []; for (let row = 0; row < rows; row++) { result[col][row] = data[row][col]; } } return result; } /** * Create frequency map of values * @param data - Array of values or objects * @param key - Property key (if data contains objects) */ export function frequency(data: T[], key?: keyof T): Map { if (!Array.isArray(data)) return new Map(); const freqMap = new Map(); data.forEach(item => { const value = key ? getNestedValue(item, key) : item; freqMap.set(value, (freqMap.get(value) || 0) + 1); }); return freqMap; } /** * Get unique values from array or object property * @param data - Array of data * @param key - Property key (optional) */ export function unique(data: T[], key?: keyof T): any[] { if (!Array.isArray(data)) return []; const values = key ? data.map(item => getNestedValue(item, key)) : data; return [...new Set(values)]; } /** * Chunk array into smaller arrays of specified size * @param data - Array to chunk * @param size - Size of each chunk */ export function chunk(data: T[], size: number): T[][] { if (!Array.isArray(data) || size <= 0) return []; const chunks: T[][] = []; for (let i = 0; i < data.length; i += size) { chunks.push(data.slice(i, i + size)); } return chunks; } /** * Zip multiple arrays together * @param arrays - Arrays to zip */ export function zip(...arrays: T[][]): T[][] { if (arrays.length === 0) return []; const length = Math.max(...arrays.map(arr => Array.isArray(arr) ? arr.length : 0)); const result: T[][] = []; for (let i = 0; i < length; i++) { result[i] = arrays.map(arr => arr[i]); } return result; } /** * Create cross product of multiple arrays * @param arrays - Arrays to get cross product from */ export function cartesianProduct(...arrays: T[][]): T[][] { if (arrays.length === 0) return []; if (arrays.some(arr => !Array.isArray(arr) || arr.length === 0)) return []; return arrays.reduce( (acc, curr) => acc.flatMap(a => curr.map(b => [...a, b])), [[]] as T[][] ); } /** * Sample random items from array * @param data - Array to sample from * @param count - Number of items to sample * @param withReplacement - Whether to allow duplicate selections */ export function sample(data: T[], count: number, withReplacement: boolean = false): T[] { if (!Array.isArray(data) || data.length === 0 || count <= 0) return []; if (withReplacement) { return Array.from({ length: count }, () => data[Math.floor(Math.random() * data.length)] ); } else { const shuffled = [...data].sort(() => Math.random() - 0.5); return shuffled.slice(0, Math.min(count, data.length)); } }