const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴"; function score(s: string, tt: string): number { let k: number | undefined; let r = 0; let run = 0; for (const c of s) { const j = tt.indexOf(c, k); // If the character isn't in the string, there's a major penalty. if (j < 0) { // The penalty scales with run length, on the assumption that we're // typing something else. // Really this should scale with the longest current run among all // search terms, but that's infeasible to implement. r -= 6 + run*run; run = 0; continue; } run++; // Characters at word boundaries get extra score. if (j == 0 || WORD_BOUNDARY.includes(tt[j-1])) { r += 2; } // As do characters that *are* word boundaries. if (WORD_BOUNDARY.includes(c)) { r += 2; } // And runs of matches scale with run length. if (j === k) { r += (run+1) * (run+1); } else { run = 0; } k = j + 1; } return r; } /** * Fuzzy string search. * @param sub Substring to search for * @param terms Iterable of values to search among * @param map Mapping from term to string to search * @returns Matching terms in decreasing match quality order */ export function stringsearch(sub: string, terms: Iterable, map: (t: T) => string): T[] { const s = sub.toLocaleLowerCase(); const scored: [T, number][] = []; for (const t of terms) { const sc = score(s, map(t).toLocaleLowerCase()); if (sc >= 0) { scored.push([t, sc]); } } return scored.sort(([, a], [, b]) => b - a).map(([t,]) => t); }