/** * Shared exercise search utilities — fuzzy matching + abbreviation expansion. * Used by both ExercisePicker (log workout) and ExercisesClient (exercise library). */ // Common gym abbreviations const ABBREVIATIONS: Record = { 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; }