(searchTerm: string, text: string)
| 8 | * Higher scores indicate better matches |
| 9 | */ |
| 10 | export function fuzzyScore(searchTerm: string, text: string): number { |
| 11 | const search = ((searchTerm ?? '') + '').trim().toLowerCase() |
| 12 | if (!search) return 0 |
| 13 | const target = ((text ?? '') + '').toLowerCase() |
| 14 | |
| 15 | let score = 0 |
| 16 | let searchIndex = 0 |
| 17 | let firstMatchIndex = -1 |
| 18 | let lastMatchIndex = -1 |
| 19 | let consecutiveMatches = 0 |
| 20 | |
| 21 | // Check for exact substring match |
| 22 | const exactMatchIndex = target.indexOf(search) |
| 23 | if (exactMatchIndex !== -1) { |
| 24 | score = 1000 |
| 25 | // Bonus for match at start of string |
| 26 | if (exactMatchIndex === 0) { |
| 27 | score += 200 |
| 28 | } |
| 29 | // Bonus for match at start of word |
| 30 | else if (target[exactMatchIndex - 1] === ' ' || target[exactMatchIndex - 1] === '-' || target[exactMatchIndex - 1] === '_') { |
| 31 | score += 100 |
| 32 | } |
| 33 | // Penalty for how far into the string the match is |
| 34 | score -= exactMatchIndex * 2 |
| 35 | // Penalty for length difference (shorter target = better match) |
| 36 | score -= (target.length - search.length) * 3 |
| 37 | return score |
| 38 | } |
| 39 | |
| 40 | // Fuzzy matching with character-by-character scoring |
| 41 | for (let i = 0; i < target.length && searchIndex < search.length; i++) { |
| 42 | if (target[i] === search[searchIndex]) { |
| 43 | // Base score for character match |
| 44 | score += 10 |
| 45 | |
| 46 | // Bonus for consecutive matches |
| 47 | if (lastMatchIndex === i - 1) { |
| 48 | consecutiveMatches++ |
| 49 | score += 5 + consecutiveMatches * 2 // Increasing bonus for longer sequences |
| 50 | } else { |
| 51 | consecutiveMatches = 0 |
| 52 | } |
| 53 | |
| 54 | // Bonus for match at start of string |
| 55 | if (i === 0) { |
| 56 | score += 20 |
| 57 | } |
| 58 | |
| 59 | // Bonus for match after space or special character (word boundary) |
| 60 | if (i > 0 && (target[i - 1] === ' ' || target[i - 1] === '-' || target[i - 1] === '_')) { |
| 61 | score += 15 |
| 62 | } |
| 63 | |
| 64 | if (firstMatchIndex === -1) firstMatchIndex = i |
| 65 | lastMatchIndex = i |
| 66 | searchIndex++ |
| 67 | } |
no outgoing calls
no test coverage detected