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:
63
projects/ui-data-utils/README.md
Normal file
63
projects/ui-data-utils/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# UiDataUtils
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.0.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the library, run:
|
||||
|
||||
```bash
|
||||
ng build ui-data-utils
|
||||
```
|
||||
|
||||
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
||||
|
||||
### Publishing the Library
|
||||
|
||||
Once the project is built, you can publish your library by following these steps:
|
||||
|
||||
1. Navigate to the `dist` directory:
|
||||
```bash
|
||||
cd dist/ui-data-utils
|
||||
```
|
||||
|
||||
2. Run the `npm publish` command to publish your library to the npm registry:
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
7
projects/ui-data-utils/ng-package.json
Normal file
7
projects/ui-data-utils/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/ui-data-utils",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
14
projects/ui-data-utils/package.json
Normal file
14
projects/ui-data-utils/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "ui-data-utils",
|
||||
"version": "0.0.1",
|
||||
"description": "Data manipulation helpers for sorting, filtering, pagination, and transformation",
|
||||
"keywords": ["angular", "data", "utilities", "sorting", "filtering", "pagination", "transformation"],
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/core": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
360
projects/ui-data-utils/src/lib/filtering.ts
Normal file
360
projects/ui-data-utils/src/lib/filtering.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Filtering utilities for data manipulation
|
||||
*/
|
||||
|
||||
import { FilterConfig, FilterOperator, 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 convertFilterValue(value: any, dataType: DataType): any {
|
||||
if (value == null) return value;
|
||||
|
||||
switch (dataType) {
|
||||
case 'string':
|
||||
return String(value);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value matches filter condition
|
||||
* @param itemValue - Value from the data item
|
||||
* @param filterValue - Value to compare against
|
||||
* @param operator - Filter operator
|
||||
* @param dataType - Data type for proper comparison
|
||||
*/
|
||||
function matchesFilter(
|
||||
itemValue: any,
|
||||
filterValue: any,
|
||||
operator: FilterOperator,
|
||||
dataType: DataType = 'string'
|
||||
): boolean {
|
||||
// Handle empty values
|
||||
if (operator === 'is_empty') {
|
||||
return itemValue == null || itemValue === '' || (Array.isArray(itemValue) && itemValue.length === 0);
|
||||
}
|
||||
|
||||
if (operator === 'is_not_empty') {
|
||||
return itemValue != null && itemValue !== '' && !(Array.isArray(itemValue) && itemValue.length === 0);
|
||||
}
|
||||
|
||||
// Convert values for comparison
|
||||
const convertedItemValue = convertFilterValue(itemValue, dataType);
|
||||
|
||||
// Handle array filter values (for 'in' and 'not_in')
|
||||
if (Array.isArray(filterValue)) {
|
||||
const convertedFilterValues = filterValue.map(v => convertFilterValue(v, dataType));
|
||||
|
||||
switch (operator) {
|
||||
case 'in':
|
||||
return convertedFilterValues.some(fv =>
|
||||
dataType === 'string'
|
||||
? convertedItemValue?.toLowerCase() === fv?.toLowerCase()
|
||||
: convertedItemValue === fv
|
||||
);
|
||||
case 'not_in':
|
||||
return !convertedFilterValues.some(fv =>
|
||||
dataType === 'string'
|
||||
? convertedItemValue?.toLowerCase() === fv?.toLowerCase()
|
||||
: convertedItemValue === fv
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const convertedFilterValue = convertFilterValue(filterValue, dataType);
|
||||
|
||||
// Handle between operator (expects array with 2 values)
|
||||
if (operator === 'between') {
|
||||
if (!Array.isArray(filterValue) || filterValue.length !== 2) return false;
|
||||
const [min, max] = filterValue.map(v => convertFilterValue(v, dataType));
|
||||
return convertedItemValue >= min && convertedItemValue <= max;
|
||||
}
|
||||
|
||||
// Handle null/undefined item values for operators that don't handle empty values
|
||||
if (convertedItemValue == null) {
|
||||
return operator === 'not_equals';
|
||||
}
|
||||
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return dataType === 'string'
|
||||
? convertedItemValue?.toLowerCase() === convertedFilterValue?.toLowerCase()
|
||||
: convertedItemValue === convertedFilterValue;
|
||||
|
||||
case 'not_equals':
|
||||
return dataType === 'string'
|
||||
? convertedItemValue?.toLowerCase() !== convertedFilterValue?.toLowerCase()
|
||||
: convertedItemValue !== convertedFilterValue;
|
||||
|
||||
case 'contains':
|
||||
if (dataType !== 'string') return false;
|
||||
return convertedItemValue?.toLowerCase()?.includes(convertedFilterValue?.toLowerCase()) || false;
|
||||
|
||||
case 'not_contains':
|
||||
if (dataType !== 'string') return true;
|
||||
return !convertedItemValue?.toLowerCase()?.includes(convertedFilterValue?.toLowerCase());
|
||||
|
||||
case 'starts_with':
|
||||
if (dataType !== 'string') return false;
|
||||
return convertedItemValue?.toLowerCase()?.startsWith(convertedFilterValue?.toLowerCase()) || false;
|
||||
|
||||
case 'ends_with':
|
||||
if (dataType !== 'string') return false;
|
||||
return convertedItemValue?.toLowerCase()?.endsWith(convertedFilterValue?.toLowerCase()) || false;
|
||||
|
||||
case 'greater_than':
|
||||
return convertedItemValue > convertedFilterValue;
|
||||
|
||||
case 'greater_than_equal':
|
||||
return convertedItemValue >= convertedFilterValue;
|
||||
|
||||
case 'less_than':
|
||||
return convertedItemValue < convertedFilterValue;
|
||||
|
||||
case 'less_than_equal':
|
||||
return convertedItemValue <= convertedFilterValue;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array by single property
|
||||
* @param data - Array to filter
|
||||
* @param config - Filter configuration
|
||||
*/
|
||||
export function filterBy<T>(data: T[], config: FilterConfig<T>): T[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return data;
|
||||
|
||||
return data.filter(item => {
|
||||
const itemValue = getNestedValue(item, config.key);
|
||||
return matchesFilter(
|
||||
itemValue,
|
||||
config.value,
|
||||
config.operator,
|
||||
config.dataType || 'string'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array by multiple conditions (AND logic)
|
||||
* @param data - Array to filter
|
||||
* @param configs - Array of filter configurations
|
||||
*/
|
||||
export function filterByMultiple<T>(data: T[], configs: FilterConfig<T>[]): T[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return data;
|
||||
if (!configs || configs.length === 0) return data;
|
||||
|
||||
return data.filter(item => {
|
||||
return configs.every(config => {
|
||||
const itemValue = getNestedValue(item, config.key);
|
||||
return matchesFilter(
|
||||
itemValue,
|
||||
config.value,
|
||||
config.operator,
|
||||
config.dataType || 'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array by multiple conditions with OR logic
|
||||
* @param data - Array to filter
|
||||
* @param configs - Array of filter configurations
|
||||
*/
|
||||
export function filterByAny<T>(data: T[], configs: FilterConfig<T>[]): T[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return data;
|
||||
if (!configs || configs.length === 0) return data;
|
||||
|
||||
return data.filter(item => {
|
||||
return configs.some(config => {
|
||||
const itemValue = getNestedValue(item, config.key);
|
||||
return matchesFilter(
|
||||
itemValue,
|
||||
config.value,
|
||||
config.operator,
|
||||
config.dataType || 'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search filter - searches across multiple properties
|
||||
* @param data - Array to search
|
||||
* @param searchTerm - Search term
|
||||
* @param searchKeys - Properties to search in
|
||||
* @param caseSensitive - Whether search is case sensitive
|
||||
*/
|
||||
export function searchFilter<T>(
|
||||
data: T[],
|
||||
searchTerm: string,
|
||||
searchKeys: (keyof T)[],
|
||||
caseSensitive: boolean = false
|
||||
): T[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return data;
|
||||
if (!searchTerm || searchTerm.trim() === '') return data;
|
||||
if (!searchKeys || searchKeys.length === 0) return data;
|
||||
|
||||
const term = caseSensitive ? searchTerm : searchTerm.toLowerCase();
|
||||
|
||||
return data.filter(item => {
|
||||
return searchKeys.some(key => {
|
||||
const value = getNestedValue(item, key);
|
||||
if (value == null) return false;
|
||||
|
||||
const stringValue = String(value);
|
||||
const searchValue = caseSensitive ? stringValue : stringValue.toLowerCase();
|
||||
return searchValue.includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced search with highlighting matches
|
||||
* @param data - Array to search
|
||||
* @param searchTerm - Search term
|
||||
* @param searchKeys - Properties to search in
|
||||
* @param caseSensitive - Whether search is case sensitive
|
||||
*/
|
||||
export function searchWithHighlight<T extends Record<string, any>>(
|
||||
data: T[],
|
||||
searchTerm: string,
|
||||
searchKeys: (keyof T)[],
|
||||
caseSensitive: boolean = false
|
||||
): Array<T & { _searchMatches: { [key: string]: string } }> {
|
||||
if (!Array.isArray(data) || data.length === 0) return [];
|
||||
if (!searchTerm || searchTerm.trim() === '') {
|
||||
return data.map(item => ({ ...item, _searchMatches: {} }));
|
||||
}
|
||||
if (!searchKeys || searchKeys.length === 0) return [];
|
||||
|
||||
const term = caseSensitive ? searchTerm : searchTerm.toLowerCase();
|
||||
const highlightStartTag = '<mark>';
|
||||
const highlightEndTag = '</mark>';
|
||||
|
||||
return data.filter(item => {
|
||||
const matches: { [key: string]: string } = {};
|
||||
let hasMatch = false;
|
||||
|
||||
searchKeys.forEach(key => {
|
||||
const value = getNestedValue(item, key);
|
||||
if (value == null) return;
|
||||
|
||||
const stringValue = String(value);
|
||||
const searchValue = caseSensitive ? stringValue : stringValue.toLowerCase();
|
||||
|
||||
if (searchValue.includes(term)) {
|
||||
hasMatch = true;
|
||||
// Create highlighted version
|
||||
const regex = new RegExp(
|
||||
caseSensitive ? searchTerm : searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
||||
caseSensitive ? 'g' : 'gi'
|
||||
);
|
||||
matches[key as string] = stringValue.replace(regex, `${highlightStartTag}$&${highlightEndTag}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasMatch) {
|
||||
return Object.assign(item, { _searchMatches: matches });
|
||||
}
|
||||
return false;
|
||||
}).map(item => item as T & { _searchMatches: { [key: string]: string } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter predicate function
|
||||
* @param config - Filter configuration
|
||||
*/
|
||||
export function createFilterPredicate<T>(config: FilterConfig<T>) {
|
||||
return (item: T): boolean => {
|
||||
const itemValue = getNestedValue(item, config.key);
|
||||
return matchesFilter(
|
||||
itemValue,
|
||||
config.value,
|
||||
config.operator,
|
||||
config.dataType || 'string'
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique values from a property across all items
|
||||
* @param data - Array of data
|
||||
* @param key - Property key
|
||||
*/
|
||||
export function getUniqueValues<T>(data: T[], key: keyof T): any[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return [];
|
||||
|
||||
const values = new Set();
|
||||
data.forEach(item => {
|
||||
const value = getNestedValue(item, key);
|
||||
if (value != null) {
|
||||
values.add(value);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(values).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter suggestions based on current data and partial input
|
||||
* @param data - Array of data
|
||||
* @param key - Property key
|
||||
* @param partialValue - Partial value to match
|
||||
* @param maxSuggestions - Maximum number of suggestions
|
||||
*/
|
||||
export function getFilterSuggestions<T>(
|
||||
data: T[],
|
||||
key: keyof T,
|
||||
partialValue: string,
|
||||
maxSuggestions: number = 10
|
||||
): string[] {
|
||||
if (!Array.isArray(data) || data.length === 0) return [];
|
||||
if (!partialValue || partialValue.trim() === '') return [];
|
||||
|
||||
const term = partialValue.toLowerCase();
|
||||
const suggestions = new Set<string>();
|
||||
|
||||
data.forEach(item => {
|
||||
const value = getNestedValue(item, key);
|
||||
if (value != null) {
|
||||
const stringValue = String(value);
|
||||
if (stringValue.toLowerCase().includes(term)) {
|
||||
suggestions.add(stringValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(suggestions)
|
||||
.sort()
|
||||
.slice(0, maxSuggestions);
|
||||
}
|
||||
280
projects/ui-data-utils/src/lib/pagination.ts
Normal file
280
projects/ui-data-utils/src/lib/pagination.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Pagination utilities for data manipulation
|
||||
*/
|
||||
|
||||
import { PaginationConfig, PaginationResult } from './types';
|
||||
|
||||
/**
|
||||
* Paginate data array
|
||||
* @param data - Array to paginate
|
||||
* @param config - Pagination configuration
|
||||
*/
|
||||
export function paginate<T>(data: T[], config: PaginationConfig): PaginationResult<T> {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Data must be an array');
|
||||
}
|
||||
|
||||
const { page, pageSize } = config;
|
||||
|
||||
if (page < 1) {
|
||||
throw new Error('Page number must be 1 or greater');
|
||||
}
|
||||
|
||||
if (pageSize < 1) {
|
||||
throw new Error('Page size must be 1 or greater');
|
||||
}
|
||||
|
||||
const totalItems = data.length;
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
|
||||
|
||||
const paginatedData = totalItems > 0 ? data.slice(startIndex, startIndex + pageSize) : [];
|
||||
|
||||
return {
|
||||
data: paginatedData,
|
||||
page,
|
||||
pageSize,
|
||||
totalItems,
|
||||
totalPages,
|
||||
hasPrevious: page > 1,
|
||||
hasNext: page < totalPages,
|
||||
startIndex: totalItems > 0 ? startIndex : -1,
|
||||
endIndex: totalItems > 0 ? endIndex : -1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pagination metadata without slicing data
|
||||
* @param totalItems - Total number of items
|
||||
* @param page - Current page
|
||||
* @param pageSize - Items per page
|
||||
*/
|
||||
export function calculatePages(totalItems: number, page: number, pageSize: number): Omit<PaginationResult, 'data'> {
|
||||
if (totalItems < 0) {
|
||||
throw new Error('Total items cannot be negative');
|
||||
}
|
||||
|
||||
if (page < 1) {
|
||||
throw new Error('Page number must be 1 or greater');
|
||||
}
|
||||
|
||||
if (pageSize < 1) {
|
||||
throw new Error('Page size must be 1 or greater');
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
totalItems,
|
||||
totalPages,
|
||||
hasPrevious: page > 1,
|
||||
hasNext: page < totalPages,
|
||||
startIndex: totalItems > 0 ? startIndex : -1,
|
||||
endIndex: totalItems > 0 ? endIndex : -1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page numbers for pagination display (with ellipsis logic)
|
||||
* @param currentPage - Current page
|
||||
* @param totalPages - Total number of pages
|
||||
* @param maxVisible - Maximum number of visible page numbers
|
||||
*/
|
||||
export function getPaginationRange(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
maxVisible: number = 7
|
||||
): Array<number | 'ellipsis'> {
|
||||
if (currentPage < 1 || currentPage > totalPages) {
|
||||
throw new Error('Current page is out of range');
|
||||
}
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// Show all pages if total is less than max visible
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const range: Array<number | 'ellipsis'> = [];
|
||||
const sidePages = Math.floor((maxVisible - 3) / 2); // Reserve 3 spots for first, last, and current
|
||||
|
||||
// Always show first page
|
||||
range.push(1);
|
||||
|
||||
if (currentPage <= sidePages + 2) {
|
||||
// Current page is near the beginning
|
||||
for (let i = 2; i <= Math.min(maxVisible - 1, totalPages - 1); i++) {
|
||||
range.push(i);
|
||||
}
|
||||
if (totalPages > maxVisible - 1) {
|
||||
range.push('ellipsis');
|
||||
}
|
||||
} else if (currentPage >= totalPages - sidePages - 1) {
|
||||
// Current page is near the end
|
||||
range.push('ellipsis');
|
||||
for (let i = Math.max(2, totalPages - maxVisible + 2); i <= totalPages - 1; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
} else {
|
||||
// Current page is in the middle
|
||||
range.push('ellipsis');
|
||||
for (let i = currentPage - sidePages; i <= currentPage + sidePages; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
range.push('ellipsis');
|
||||
}
|
||||
|
||||
// Always show last page (if it's not already included)
|
||||
if (totalPages > 1 && !range.includes(totalPages)) {
|
||||
range.push(totalPages);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item range text for display (e.g., "1-10 of 100")
|
||||
* @param startIndex - Start index (0-based)
|
||||
* @param endIndex - End index (0-based)
|
||||
* @param totalItems - Total number of items
|
||||
*/
|
||||
export function getItemRangeText(startIndex: number, endIndex: number, totalItems: number): string {
|
||||
if (totalItems === 0) return '0 of 0';
|
||||
|
||||
const start = startIndex + 1; // Convert to 1-based
|
||||
const end = endIndex + 1; // Convert to 1-based
|
||||
|
||||
return `${start}-${end} of ${totalItems}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page information text (e.g., "Page 1 of 10")
|
||||
* @param currentPage - Current page
|
||||
* @param totalPages - Total number of pages
|
||||
*/
|
||||
export function getPageInfoText(currentPage: number, totalPages: number): string {
|
||||
if (totalPages === 0) return 'Page 0 of 0';
|
||||
return `Page ${currentPage} of ${totalPages}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate optimal page size based on container height and item height
|
||||
* @param containerHeight - Height of the container in pixels
|
||||
* @param itemHeight - Height of each item in pixels
|
||||
* @param minPageSize - Minimum page size
|
||||
* @param maxPageSize - Maximum page size
|
||||
*/
|
||||
export function calculateOptimalPageSize(
|
||||
containerHeight: number,
|
||||
itemHeight: number,
|
||||
minPageSize: number = 5,
|
||||
maxPageSize: number = 100
|
||||
): number {
|
||||
const calculatedSize = Math.floor(containerHeight / itemHeight);
|
||||
return Math.max(minPageSize, Math.min(maxPageSize, calculatedSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific page (with bounds checking)
|
||||
* @param currentPage - Current page
|
||||
* @param totalPages - Total number of pages
|
||||
* @param targetPage - Target page to navigate to
|
||||
*/
|
||||
export function navigateToPage(currentPage: number, totalPages: number, targetPage: number): number {
|
||||
return Math.max(1, Math.min(totalPages, targetPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to previous page
|
||||
* @param currentPage - Current page
|
||||
*/
|
||||
export function navigateToPrevious(currentPage: number): number {
|
||||
return Math.max(1, currentPage - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next page
|
||||
* @param currentPage - Current page
|
||||
* @param totalPages - Total number of pages
|
||||
*/
|
||||
export function navigateToNext(currentPage: number, totalPages: number): number {
|
||||
return Math.min(totalPages, currentPage + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to first page
|
||||
*/
|
||||
export function navigateToFirst(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to last page
|
||||
* @param totalPages - Total number of pages
|
||||
*/
|
||||
export function navigateToLast(totalPages: number): number {
|
||||
return Math.max(1, totalPages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page navigation is valid
|
||||
* @param page - Page to check
|
||||
* @param totalPages - Total number of pages
|
||||
*/
|
||||
export function isValidPage(page: number, totalPages: number): boolean {
|
||||
return page >= 1 && page <= totalPages && totalPages > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible page sizes (common values)
|
||||
*/
|
||||
export function getStandardPageSizes(): number[] {
|
||||
return [5, 10, 20, 25, 50, 100];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended page size based on total items
|
||||
* @param totalItems - Total number of items
|
||||
*/
|
||||
export function getRecommendedPageSize(totalItems: number): number {
|
||||
if (totalItems <= 25) return 10;
|
||||
if (totalItems <= 100) return 25;
|
||||
if (totalItems <= 500) return 50;
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate with server-side pagination simulation
|
||||
* @param data - Full dataset
|
||||
* @param page - Current page
|
||||
* @param pageSize - Items per page
|
||||
* @param searchTerm - Search term to filter data first
|
||||
* @param searchKeys - Keys to search in
|
||||
*/
|
||||
export function serverPaginate<T>(
|
||||
data: T[],
|
||||
page: number,
|
||||
pageSize: number,
|
||||
searchTerm?: string,
|
||||
searchKeys?: (keyof T)[]
|
||||
): PaginationResult<T> {
|
||||
let filteredData = data;
|
||||
|
||||
// Apply search filter first if provided
|
||||
if (searchTerm && searchKeys && searchKeys.length > 0) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredData = data.filter(item => {
|
||||
return searchKeys.some(key => {
|
||||
const value = item[key];
|
||||
return value != null && String(value).toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Then paginate the filtered data
|
||||
return paginate(filteredData, { page, pageSize });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
378
projects/ui-data-utils/src/lib/transformation.ts
Normal file
378
projects/ui-data-utils/src/lib/transformation.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
130
projects/ui-data-utils/src/lib/types.ts
Normal file
130
projects/ui-data-utils/src/lib/types.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Common types used across ui-data-utils
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sort direction
|
||||
*/
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
/**
|
||||
* Supported data types for comparison
|
||||
*/
|
||||
export type DataType = 'string' | 'number' | 'date' | 'boolean';
|
||||
|
||||
/**
|
||||
* Sort configuration for single property
|
||||
*/
|
||||
export interface SortConfig<T = any> {
|
||||
/** Property key to sort by */
|
||||
key: keyof T;
|
||||
/** Sort direction */
|
||||
direction: SortDirection;
|
||||
/** Data type for proper comparison */
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-column sort configuration
|
||||
*/
|
||||
export interface MultiSortConfig<T = any> {
|
||||
/** Array of sort configurations in priority order */
|
||||
sorts: SortConfig<T>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator function type
|
||||
*/
|
||||
export type ComparatorFn<T = any> = (a: T, b: T) => number;
|
||||
|
||||
/**
|
||||
* Filter operator types
|
||||
*/
|
||||
export type FilterOperator =
|
||||
| 'equals'
|
||||
| 'not_equals'
|
||||
| 'contains'
|
||||
| 'not_contains'
|
||||
| 'starts_with'
|
||||
| 'ends_with'
|
||||
| 'greater_than'
|
||||
| 'greater_than_equal'
|
||||
| 'less_than'
|
||||
| 'less_than_equal'
|
||||
| 'between'
|
||||
| 'in'
|
||||
| 'not_in'
|
||||
| 'is_empty'
|
||||
| 'is_not_empty';
|
||||
|
||||
/**
|
||||
* Filter configuration
|
||||
*/
|
||||
export interface FilterConfig<T = any> {
|
||||
/** Property key to filter by */
|
||||
key: keyof T;
|
||||
/** Filter operator */
|
||||
operator: FilterOperator;
|
||||
/** Filter value(s) */
|
||||
value: any;
|
||||
/** Data type for proper comparison */
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination configuration
|
||||
*/
|
||||
export interface PaginationConfig {
|
||||
/** Current page (1-based) */
|
||||
page: number;
|
||||
/** Items per page */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination result
|
||||
*/
|
||||
export interface PaginationResult<T = any> {
|
||||
/** Paginated data */
|
||||
data: T[];
|
||||
/** Current page */
|
||||
page: number;
|
||||
/** Items per page */
|
||||
pageSize: number;
|
||||
/** Total number of items */
|
||||
totalItems: number;
|
||||
/** Total number of pages */
|
||||
totalPages: number;
|
||||
/** Has previous page */
|
||||
hasPrevious: boolean;
|
||||
/** Has next page */
|
||||
hasNext: boolean;
|
||||
/** Start index (0-based) */
|
||||
startIndex: number;
|
||||
/** End index (0-based) */
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregation functions
|
||||
*/
|
||||
export type AggregationFunction = 'sum' | 'avg' | 'min' | 'max' | 'count';
|
||||
|
||||
/**
|
||||
* Aggregation result
|
||||
*/
|
||||
export interface AggregationResult {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group by result
|
||||
*/
|
||||
export interface GroupByResult<T = any> {
|
||||
/** Group key */
|
||||
key: any;
|
||||
/** Items in this group */
|
||||
items: T[];
|
||||
/** Group count */
|
||||
count: number;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiDataUtilsComponent } from './ui-data-utils.component';
|
||||
|
||||
describe('UiDataUtilsComponent', () => {
|
||||
let component: UiDataUtilsComponent;
|
||||
let fixture: ComponentFixture<UiDataUtilsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UiDataUtilsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UiDataUtilsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
projects/ui-data-utils/src/lib/ui-data-utils.component.ts
Normal file
15
projects/ui-data-utils/src/lib/ui-data-utils.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-ui-data-utils',
|
||||
imports: [],
|
||||
template: `
|
||||
<p>
|
||||
ui-data-utils works!
|
||||
</p>
|
||||
`,
|
||||
styles: ``
|
||||
})
|
||||
export class UiDataUtilsComponent {
|
||||
|
||||
}
|
||||
16
projects/ui-data-utils/src/lib/ui-data-utils.service.spec.ts
Normal file
16
projects/ui-data-utils/src/lib/ui-data-utils.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiDataUtilsService } from './ui-data-utils.service';
|
||||
|
||||
describe('UiDataUtilsService', () => {
|
||||
let service: UiDataUtilsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UiDataUtilsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
9
projects/ui-data-utils/src/lib/ui-data-utils.service.ts
Normal file
9
projects/ui-data-utils/src/lib/ui-data-utils.service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UiDataUtilsService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
22
projects/ui-data-utils/src/public-api.ts
Normal file
22
projects/ui-data-utils/src/public-api.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Public API Surface of ui-data-utils
|
||||
*/
|
||||
|
||||
// Types and interfaces
|
||||
export * from './lib/types';
|
||||
|
||||
// Sorting utilities
|
||||
export * from './lib/sorting';
|
||||
|
||||
// Filtering utilities
|
||||
export * from './lib/filtering';
|
||||
|
||||
// Pagination utilities
|
||||
export * from './lib/pagination';
|
||||
|
||||
// Transformation utilities
|
||||
export * from './lib/transformation';
|
||||
|
||||
// Legacy exports (keep for compatibility)
|
||||
export * from './lib/ui-data-utils.service';
|
||||
export * from './lib/ui-data-utils.component';
|
||||
15
projects/ui-data-utils/tsconfig.lib.json
Normal file
15
projects/ui-data-utils/tsconfig.lib.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
11
projects/ui-data-utils/tsconfig.lib.prod.json
Normal file
11
projects/ui-data-utils/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,11 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
15
projects/ui-data-utils/tsconfig.spec.json
Normal file
15
projects/ui-data-utils/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user