feat : add fuzzy matching for searching
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { matchesTeamSearch, sortTeamsByDps } from "./team-search";
|
||||
import { matchesTeamSearch, searchTeams, sortTeamsByDps } from "./team-search";
|
||||
|
||||
const team = {
|
||||
team: ["Vaporize Team"],
|
||||
@@ -40,3 +40,33 @@ describe("team search", () => {
|
||||
expect(sortTeamsByDps(teams, null).map((item) => item.name)).toEqual(["middle", "best", "tie"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("team fuzzy fallback", () => {
|
||||
it("matches typos only in the selected category", () => {
|
||||
expect(searchTeams([team], (item) => item, "mulani", "character")).toEqual({ items: [team], fuzzy: true });
|
||||
expect(searchTeams([team], (item) => item, "mulani", "artifact").items).toEqual([]);
|
||||
expect(searchTeams([team], (item) => item, "mulani", "all").items).toEqual([team]);
|
||||
});
|
||||
|
||||
it("decides fallback across all eligible teams", () => {
|
||||
const direct = { ...team, character: ["Mulani"] };
|
||||
expect(searchTeams([team, direct], (item) => item, "mulani", "all")).toEqual({ items: [direct], fuzzy: false });
|
||||
expect(searchTeams([team], (item) => item, "", "all")).toEqual({ items: [team], fuzzy: false });
|
||||
expect(searchTeams([], (item) => item, "mulani", "all")).toEqual({ items: [], fuzzy: false });
|
||||
});
|
||||
|
||||
it("does not allow direct queries to span separate names", () => {
|
||||
const values = { ...team, character: ["Jean", "Mualani"] };
|
||||
expect(searchTeams([values], (item) => item, "Jean Mualani", "character").items).toEqual([]);
|
||||
});
|
||||
|
||||
it("applies DPS ordering after fuzzy matching and preserves fuzzy ranking without sorting", () => {
|
||||
const teams = [
|
||||
{ ...team, character: ["Mualani"], rotationSeconds: 10, members: [{ damage: 100 }] },
|
||||
{ ...team, character: ["Muloni"], rotationSeconds: 5, members: [{ damage: 100 }] },
|
||||
];
|
||||
const matches = searchTeams(teams, (item) => item, "mulani", "character").items;
|
||||
expect(sortTeamsByDps(matches, "best")).toEqual([teams[1], teams[0]]);
|
||||
expect(sortTeamsByDps(matches, null)).toEqual(teams);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeSearch, searchItems } from "../search";
|
||||
|
||||
export type TeamSearchTarget =
|
||||
| "all"
|
||||
| "team"
|
||||
@@ -47,7 +49,7 @@ export function matchesTeamSearch(
|
||||
query: string,
|
||||
target: TeamSearchTarget,
|
||||
): boolean {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase();
|
||||
const normalizedQuery = normalizeSearch(query);
|
||||
if (!normalizedQuery) return true;
|
||||
|
||||
const candidates = target === "all"
|
||||
@@ -55,6 +57,23 @@ export function matchesTeamSearch(
|
||||
: values[target];
|
||||
|
||||
return candidates.some((value) =>
|
||||
value.toLocaleLowerCase().includes(normalizedQuery),
|
||||
normalizeSearch(value).includes(normalizedQuery),
|
||||
);
|
||||
}
|
||||
|
||||
export function searchTeams<T>(
|
||||
teams: readonly T[],
|
||||
values: (team: T) => TeamSearchValues,
|
||||
query: string,
|
||||
target: TeamSearchTarget,
|
||||
) {
|
||||
const candidates = (team: T) => {
|
||||
const fields = values(team);
|
||||
return target === "all" ? Object.values(fields).flat() : fields[target];
|
||||
};
|
||||
// Separate candidates retain the existing rule that a direct match must
|
||||
// occur within one name, rather than spanning two members or categories.
|
||||
const direct = teams.filter((team) => matchesTeamSearch(values(team), query, target));
|
||||
if (direct.length || !query.trim()) return { items: direct, fuzzy: false };
|
||||
return searchItems(teams, query, () => "", candidates);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user