export function fuzzyMatch(text: string, query: string): { matches: boolean, score: number } { if (!query) return { matches: true, score: 0 } const textLower = text.toLowerCase() const queryLower = query.toLowerCase() if (textLower.includes(queryLower)) return { matches: true, score: 1000 } const queryWords = queryLower.split(/\s+/).filter(w => w.length > 0) const textWords = textLower.split(/\s+/) let matchedWords = 0 for (const queryWord of queryWords) { if (textWords.some(textWord => textWord.includes(queryWord))) matchedWords++ } if (matchedWords === queryWords.length) return { matches: true, score: 100 + matchedWords * 10 } let queryIndex = 0 let consecutiveMatches = 0 let maxConsecutive = 0 let totalMatches = 0 for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) { if (textLower[i] === queryLower[queryIndex]) { queryIndex++ consecutiveMatches++ totalMatches++ maxConsecutive = Math.max(maxConsecutive, consecutiveMatches) } else { consecutiveMatches = 0 } } if (queryIndex === queryLower.length) return { matches: true, score: maxConsecutive * 5 + totalMatches } return { matches: false, score: 0 } } export function strJoin(...parts: (string | null | undefined | false | 0)[]): string { return parts.filter(Boolean).join(', ') } export function uuid4(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID() return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) }) }