77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
/**
|
|
* Shared exercise search utilities — fuzzy matching + abbreviation expansion.
|
|
* Used by both ExercisePicker (log workout) and ExercisesClient (exercise library).
|
|
*/
|
|
|
|
// Common gym abbreviations
|
|
const ABBREVIATIONS: Record<string, string> = {
|
|
kb: "kettlebell",
|
|
db: "dumbbell",
|
|
bb: "barbell",
|
|
ghd: "glute ham developer",
|
|
rdl: "romanian deadlift",
|
|
ohp: "overhead press",
|
|
};
|
|
|
|
/**
|
|
* Expand a search query into multiple variants using known abbreviations.
|
|
* e.g. "kb swing" → ["kb swing", "kettlebell swing"]
|
|
*/
|
|
export function expandAbbreviations(query: string): string[] {
|
|
const lower = query.toLowerCase().trim();
|
|
const variants: string[] = [lower];
|
|
|
|
// Check if the whole query is an abbreviation
|
|
if (ABBREVIATIONS[lower]) {
|
|
variants.push(ABBREVIATIONS[lower]);
|
|
}
|
|
|
|
// Check if the query starts with an abbreviation followed by a space
|
|
for (const [abbr, full] of Object.entries(ABBREVIATIONS)) {
|
|
if (lower.startsWith(abbr + " ")) {
|
|
variants.push(full + lower.slice(abbr.length));
|
|
}
|
|
}
|
|
|
|
return variants;
|
|
}
|
|
|
|
/**
|
|
* Score how well a query matches a target string.
|
|
* Lower = better match. Returns -1 for no match.
|
|
*
|
|
* Priority: exact match (0) > starts with (1) > word starts with (2) > substring (3) > fuzzy chars (4+)
|
|
*/
|
|
export function fuzzyScore(query: string, target: string): number {
|
|
const q = query.toLowerCase();
|
|
const t = target.toLowerCase();
|
|
|
|
if (t === q) return 0;
|
|
if (t.startsWith(q)) return 1;
|
|
|
|
const words = t.split(/\s+/);
|
|
if (words.some((w) => w.startsWith(q))) return 2;
|
|
if (t.includes(q)) return 3;
|
|
|
|
// Fuzzy character match
|
|
let qi = 0;
|
|
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
if (t[ti] === q[qi]) qi++;
|
|
}
|
|
|
|
return qi === q.length ? 4 + (t.length - q.length) : -1;
|
|
}
|
|
|
|
/**
|
|
* Search exercises using fuzzy matching + abbreviation expansion.
|
|
* Returns scored exercises sorted by best match, or -1 for no match.
|
|
*/
|
|
export function scoreExercise(query: string, exerciseName: string): number {
|
|
const variants = expandAbbreviations(query);
|
|
const scores = variants
|
|
.map((q) => fuzzyScore(q, exerciseName))
|
|
.filter((s) => s >= 0);
|
|
|
|
return scores.length > 0 ? Math.min(...scores) : -1;
|
|
}
|