/** * Sorting utilities for data manipulation */ import { SortConfig, MultiSortConfig, ComparatorFn, SortDirection, DataType } 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]; } /** * Convert value to appropriate type for comparison * @param value - Value to convert * @param dataType - Target data type */ function convertValue(value: any, dataType: DataType): any { if (value == null) return value; switch (dataType) { case 'string': return String(value).toLowerCase(); case 'number': const num = Number(value); return isNaN(num) ? 0 : num; case 'date': if (value instanceof Date) return value; const date = new Date(value); return isNaN(date.getTime()) ? new Date(0) : date; case 'boolean': return Boolean(value); default: return value; } } /** * Create a comparator function for a specific property and direction * @param key - Property key to sort by * @param direction - Sort direction * @param dataType - Data type for proper comparison */ export function createComparator( key: keyof T, direction: SortDirection = 'asc', dataType: DataType = 'string' ): ComparatorFn { return (a: T, b: T): number => { const valueA = convertValue(getNestedValue(a, key), dataType); const valueB = convertValue(getNestedValue(b, key), dataType); // Handle null/undefined values if (valueA == null && valueB == null) return 0; if (valueA == null) return direction === 'asc' ? -1 : 1; if (valueB == null) return direction === 'asc' ? 1 : -1; let result = 0; if (dataType === 'string') { result = valueA.localeCompare(valueB); } else if (dataType === 'date') { result = valueA.getTime() - valueB.getTime(); } else { // For numbers and booleans if (valueA < valueB) result = -1; else if (valueA > valueB) result = 1; else result = 0; } return direction === 'desc' ? -result : result; }; } /** * Sort array by single property * @param data - Array to sort * @param config - Sort configuration */ export function sortBy(data: T[], config: SortConfig): T[] { if (!Array.isArray(data) || data.length === 0) return data; const comparator = createComparator( config.key, config.direction, config.dataType || 'string' ); return [...data].sort(comparator); } /** * Sort array by multiple properties with priority * @param data - Array to sort * @param config - Multi-sort configuration */ export function sortByMultiple(data: T[], config: MultiSortConfig): T[] { if (!Array.isArray(data) || data.length === 0) return data; if (!config.sorts || config.sorts.length === 0) return data; // Create comparators for each sort configuration const comparators = config.sorts.map(sortConfig => createComparator( sortConfig.key, sortConfig.direction, sortConfig.dataType || 'string' ) ); return [...data].sort((a: T, b: T): number => { // Apply each comparator in order until we find a non-zero result for (const comparator of comparators) { const result = comparator(a, b); if (result !== 0) return result; } return 0; }); } /** * Create a stable sort function that maintains relative order of equal elements * @param data - Array to sort * @param comparator - Comparison function */ export function stableSort(data: T[], comparator: ComparatorFn): T[] { if (!Array.isArray(data) || data.length === 0) return data; // Add index to maintain stability const indexed = data.map((item, index) => ({ item, index })); indexed.sort((a, b) => { const result = comparator(a.item, b.item); // If items are equal, maintain original order return result !== 0 ? result : a.index - b.index; }); return indexed.map(({ item }) => item); } /** * Check if an array is sorted according to a comparator * @param data - Array to check * @param comparator - Comparison function */ export function isSorted(data: T[], comparator: ComparatorFn): boolean { if (!Array.isArray(data) || data.length <= 1) return true; for (let i = 1; i < data.length; i++) { if (comparator(data[i - 1], data[i]) > 0) return false; } return true; } /** * Reverse sort direction * @param direction - Current direction */ export function reverseDirection(direction: SortDirection): SortDirection { return direction === 'asc' ? 'desc' : 'asc'; } /** * Get sort direction for next click (for UI toggling) * @param currentDirection - Current sort direction * @param allowUnsorted - Whether to allow unsorted state */ export function getNextSortDirection( currentDirection: SortDirection | null, allowUnsorted: boolean = true ): SortDirection | null { if (currentDirection === null) return 'asc'; if (currentDirection === 'asc') return 'desc'; if (currentDirection === 'desc' && allowUnsorted) return null; return 'asc'; // Cycle back to asc if unsorted not allowed } /** * Sort array by multiple keys with individual directions * @param data - Array to sort * @param keys - Array of keys to sort by * @param directions - Array of directions (defaults to 'asc' for all) * @param dataTypes - Array of data types (defaults to 'string' for all) */ export function sortByKeys( data: T[], keys: (keyof T)[], directions: SortDirection[] = [], dataTypes: DataType[] = [] ): T[] { if (!Array.isArray(data) || data.length === 0) return data; if (!keys || keys.length === 0) return data; const sorts: SortConfig[] = keys.map((key, index) => ({ key, direction: directions[index] || 'asc', dataType: dataTypes[index] || 'string' })); return sortByMultiple(data, { sorts }); }