Files
proof-of-work/workout-planner/lib/exerciseSearch.ts
T
2026-02-28 09:27:26 -06:00

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;
}