Add comprehensive library expansion with new components and demos
- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio - Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout - Add comprehensive demo components for all new features - Update project configuration and dependencies - Expand component exports and routing structure - Add UI landing pages planning document 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
206
projects/ui-data-utils/src/lib/sorting.ts
Normal file
206
projects/ui-data-utils/src/lib/sorting.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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<T = any>(
|
||||
key: keyof T,
|
||||
direction: SortDirection = 'asc',
|
||||
dataType: DataType = 'string'
|
||||
): ComparatorFn<T> {
|
||||
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<T>(data: T[], config: SortConfig<T>): 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<T>(data: T[], config: MultiSortConfig<T>): 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<T>(data: T[], comparator: ComparatorFn<T>): 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<T>(data: T[], comparator: ComparatorFn<T>): 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<T>(
|
||||
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<T>[] = keys.map((key, index) => ({
|
||||
key,
|
||||
direction: directions[index] || 'asc',
|
||||
dataType: dataTypes[index] || 'string'
|
||||
}));
|
||||
|
||||
return sortByMultiple(data, { sorts });
|
||||
}
|
||||
Reference in New Issue
Block a user