Files
ui-essentials/projects/ui-data-utils/src/lib/transformation.ts
skyai_dev 5346d6d0c9 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>
2025-09-05 05:37:37 +10:00

378 lines
9.9 KiB
TypeScript

/**
* 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<T>(data: T[], key: keyof T): GroupByResult<T>[] {
if (!Array.isArray(data) || data.length === 0) return [];
const groups = new Map<any, T[]>();
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<T>(data: T[], keys: (keyof T)[]): GroupByResult<T>[] {
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<string, T[]>();
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<T>(
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<T>(
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<T extends Record<string, any>, K extends keyof T>(data: T[], keys: K[]): Pick<T, K>[] {
if (!Array.isArray(data) || data.length === 0) return [];
if (!keys || keys.length === 0) return [];
return data.map(item => {
const result = {} as Pick<T, K>;
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<T, K extends keyof T>(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<T>(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<T>(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<T>(
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<T>(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<T>(data: T[], key?: keyof T): Map<any, number> {
if (!Array.isArray(data)) return new Map();
const freqMap = new Map<any, number>();
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<T>(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<T>(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<T>(...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<T>(...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<T>(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));
}
}