ad58edbfe1
Fixes #20.
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
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<T>(sub: string, terms: Iterable<T>, 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);
|
|
}
|