97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
/**
|
|
* Aggregate result type definition
|
|
*/
|
|
export interface AggregateResult {
|
|
key: string;
|
|
count: number;
|
|
values: any[];
|
|
average?: number;
|
|
}
|
|
|
|
interface AggregateOptions {
|
|
collectValues?: boolean;
|
|
}
|
|
|
|
/**
|
|
* DataProcessor:
|
|
* Core aggregation engine for maximizing algorithmic efficiency and maintainability.
|
|
* Guarantees O(N) complexity with data distribution-sensitive optimization strategy.
|
|
*/
|
|
export class DataProcessor {
|
|
/**
|
|
* Core data aggregation function (Optimized O(N))
|
|
* @param data Array to aggregate
|
|
* @param keyPath Property path to group by
|
|
*/
|
|
public static aggregate(data: any[], keyPath: string, options: AggregateOptions = {}): AggregateResult[] {
|
|
if (!data || data.length === 0) return [];
|
|
if (!Array.isArray(data)) {
|
|
throw new TypeError('DataProcessor.aggregate expects data to be an array.');
|
|
}
|
|
|
|
const pathSegments = this.parseKeyPath(keyPath);
|
|
const collectValues = options.collectValues !== false;
|
|
|
|
const map = new Map<string, AggregateResult>();
|
|
|
|
for (const item of data) {
|
|
try {
|
|
const keyValue = this.getNestedValue(item, pathSegments);
|
|
if (keyValue === undefined || keyValue === null) continue;
|
|
|
|
const key = String(keyValue);
|
|
let entry = map.get(key);
|
|
|
|
if (!entry) {
|
|
entry = {
|
|
key,
|
|
count: 0,
|
|
values: []
|
|
};
|
|
map.set(key, entry);
|
|
}
|
|
|
|
entry.count++;
|
|
if (collectValues) {
|
|
entry.values.push(item);
|
|
}
|
|
|
|
} catch (error) {
|
|
// Isolate item processing failures to prevent entire aggregation from aborting
|
|
console.warn(`[DataProcessor] Skip item due to error: ${error}`);
|
|
}
|
|
}
|
|
|
|
return Array.from(map.values());
|
|
}
|
|
|
|
/**
|
|
* Nested object property accessor (safety handling).
|
|
* keyPath is parsed once outside the loop to avoid repeated split costs on large datasets.
|
|
*/
|
|
private static getNestedValue(obj: any, pathSegments: string[]): any {
|
|
return pathSegments.reduce((prev, curr) => {
|
|
if (prev === undefined || prev === null) return undefined;
|
|
return prev[curr];
|
|
}, obj);
|
|
}
|
|
|
|
private static parseKeyPath(keyPath: string): string[] {
|
|
if (typeof keyPath !== 'string' || keyPath.trim().length === 0) {
|
|
throw new TypeError('DataProcessor.aggregate expects keyPath to be a non-empty string.');
|
|
}
|
|
|
|
const segments = keyPath.split('.').map(segment => segment.trim());
|
|
if (segments.some(segment => segment.length === 0)) {
|
|
throw new TypeError('DataProcessor.aggregate received an invalid keyPath.');
|
|
}
|
|
|
|
const forbiddenSegments = new Set(['__proto__', 'prototype', 'constructor']);
|
|
if (segments.some(segment => forbiddenSegments.has(segment))) {
|
|
throw new TypeError('DataProcessor.aggregate received an unsafe keyPath.');
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
}
|