/** * Utility Functions */ // ============================================================================= // ID GENERATION // ============================================================================= export function generateId(prefix?: string): string { const timestamp = Date.now().toString(36) const random = Math.random().toString(36).substring(2, 9) return prefix ? `${prefix}-${timestamp}-${random}` : `${timestamp}-${random}` } export function generateUUID(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID() } // Fallback for older environments return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } // ============================================================================= // DATE UTILITIES // ============================================================================= export function formatDate(date: Date | string, locale = 'de-DE'): string { const d = typeof date === 'string' ? new Date(date) : date return d.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', }) } export function formatDateTime(date: Date | string, locale = 'de-DE'): string { const d = typeof date === 'string' ? new Date(date) : date return d.toLocaleString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } export function isDateExpired(date: Date | string): boolean { const d = typeof date === 'string' ? new Date(date) : date return d < new Date() } export function addDays(date: Date, days: number): Date { const result = new Date(date) result.setDate(result.getDate() + days) return result } export function daysBetween(date1: Date, date2: Date): number { const oneDay = 24 * 60 * 60 * 1000 return Math.round(Math.abs((date1.getTime() - date2.getTime()) / oneDay)) } // ============================================================================= // STRING UTILITIES // ============================================================================= export function truncate(str: string, maxLength: number, suffix = '...'): string { if (str.length <= maxLength) return str return str.substring(0, maxLength - suffix.length) + suffix } export function slugify(str: string): string { return str .toLowerCase() .replace(/[äöü]/g, c => ({ ä: 'ae', ö: 'oe', ü: 'ue' })[c] || c) .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') } export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } // ============================================================================= // ARRAY UTILITIES // ============================================================================= export function groupBy( array: T[], keyFn: (item: T) => K ): Record { return array.reduce( (groups, item) => { const key = keyFn(item) if (!groups[key]) { groups[key] = [] } groups[key].push(item) return groups }, {} as Record ) } export function uniqueBy(array: T[], keyFn: (item: T) => unknown): T[] { const seen = new Set() return array.filter(item => { const key = keyFn(item) if (seen.has(key)) return false seen.add(key) return true }) } export function sortBy(array: T[], keyFn: (item: T) => number | string, desc = false): T[] { return [...array].sort((a, b) => { const aKey = keyFn(a) const bKey = keyFn(b) if (aKey < bKey) return desc ? 1 : -1 if (aKey > bKey) return desc ? -1 : 1 return 0 }) } // ============================================================================= // OBJECT UTILITIES // ============================================================================= export function deepClone(obj: T): T { if (obj === null || typeof obj !== 'object') return obj if (obj instanceof Date) return new Date(obj.getTime()) as T if (Array.isArray(obj)) return obj.map(item => deepClone(item)) as T return Object.fromEntries( Object.entries(obj as object).map(([key, value]) => [key, deepClone(value)]) ) as T } export function deepMerge>(target: T, source: Partial): T { const result = { ...target } for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { const sourceValue = source[key] const targetValue = target[key] if ( typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue) ) { result[key] = deepMerge( targetValue as Record, sourceValue as Record ) as T[Extract] } else { result[key] = sourceValue as T[Extract] } } } return result } export function pick(obj: T, keys: K[]): Pick { return keys.reduce( (result, key) => { if (key in obj) { result[key] = obj[key] } return result }, {} as Pick ) } export function omit(obj: T, keys: K[]): Omit { const result = { ...obj } keys.forEach(key => delete result[key]) return result } // ============================================================================= // VALIDATION UTILITIES // ============================================================================= export function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ return emailRegex.test(email) } export function isValidUrl(url: string): boolean { try { new URL(url) return true } catch { return false } } export function isEmpty(value: unknown): boolean { if (value === null || value === undefined) return true if (typeof value === 'string') return value.trim().length === 0 if (Array.isArray(value)) return value.length === 0 if (typeof value === 'object') return Object.keys(value).length === 0 return false } // ============================================================================= // ASYNC UTILITIES // ============================================================================= export function debounce unknown>( fn: T, delay: number ): (...args: Parameters) => void { let timeoutId: ReturnType return (...args: Parameters) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn(...args), delay) } } export function throttle unknown>( fn: T, limit: number ): (...args: Parameters) => void { let lastCall = 0 return (...args: Parameters) => { const now = Date.now() if (now - lastCall >= limit) { lastCall = now fn(...args) } } } export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } export async function retry( fn: () => Promise, maxRetries: number, delay: number ): Promise { let lastError: Error | null = null for (let i = 0; i <= maxRetries; i++) { try { return await fn() } catch (error) { lastError = error as Error if (i < maxRetries) { await sleep(delay * Math.pow(2, i)) } } } throw lastError }