diff --git a/docs/user_guide/docs/goals.es.md b/docs/user_guide/docs/goals.es.md index 85f4b8648d..fc2343e32f 100644 --- a/docs/user_guide/docs/goals.es.md +++ b/docs/user_guide/docs/goals.es.md @@ -27,8 +27,8 @@ There are icons at the top of each column to ![Review Entries column sort icon](../images/reviewEntriesColumnSort.png){width=20} sort the data. In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter -with a text search.By default, the text search is a fuzzy match: it is not case sensitive and it allows for a single -typo. If you want exact text matches, use quotes around your filter. +with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two +typos. If you want exact text matches, use quotes around your filter. In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings that entries have. In the Pronunciations column, you can also filter by speaker name. diff --git a/docs/user_guide/docs/goals.md b/docs/user_guide/docs/goals.md index d419db7cb9..b7ff8e3051 100644 --- a/docs/user_guide/docs/goals.md +++ b/docs/user_guide/docs/goals.md @@ -26,8 +26,8 @@ There are icons at the top of each column to ![Review Entries column sort icon](images/reviewEntriesColumnSort.png){width=20} sort the data. In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter -with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for a single -typo. If you want exact text matches, use quotes around your filter. +with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two +typos. If you want exact text matches, use quotes around your filter. In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings that entries have. In the Pronunciations column, you can also filter by speaker name. diff --git a/docs/user_guide/docs/goals.zh.md b/docs/user_guide/docs/goals.zh.md index d9c99bb681..b5cb930de0 100644 --- a/docs/user_guide/docs/goals.zh.md +++ b/docs/user_guide/docs/goals.zh.md @@ -26,8 +26,8 @@ There are icons at the top of each column to ![Review Entries column sort icon](../images/reviewEntriesColumnSort.png){width=20} sort the data. In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter -with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for a single -typo. If you want exact text matches, use quotes around your filter. +with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two +typos. If you want exact text matches, use quotes around your filter. In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings that entries have. In the Pronunciations column, you can also filter by speaker name. diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/filterFn.ts b/src/goals/ReviewEntries/ReviewEntriesTable/filterFn.ts index 9cdf4a8bf2..b400d51fb5 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/filterFn.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTable/filterFn.ts @@ -19,22 +19,22 @@ export function isQuoted(filter: string): boolean { return /^["'\p{Pi}].*["'\p{Pf}]$/u.test(filter); } +/** Number of typos allowed, depending on filter-length. */ +function levDist(len: number): number { + return len < 3 ? 0 : len < 6 ? 1 : 2; +} + /** Checks if value contains a substring that fuzzy-matches the filter. */ -export function fuzzyContains( - value: string, - filter: string, - levenshteinDistance = 1 -): boolean { +export function fuzzyContains(value: string, filter: string): boolean { + filter = filter.toLowerCase(); + value = value.toLowerCase(); // `fuzzySearch(...)` returns a generator; // `.next()` on a generator always returns an object with boolean property `done` - return !fuzzySearch( - filter.toLowerCase(), - value.toLowerCase(), - levenshteinDistance - ).next().done; + return !fuzzySearch(filter, value, levDist(filter.length)).next().done; } -/** Check if string matches filter. */ +/** Check if string matches filter. + * If filter quoted, exact match. Otherwise, fuzzy match. */ export function matchesFilter(value: string, filter: string): boolean { filter = filter.trim(); return isQuoted(filter) diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/tests/filterFn.test.ts b/src/goals/ReviewEntries/ReviewEntriesTable/tests/filterFn.test.ts index 8c7f2490ce..f5ccd5092d 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/tests/filterFn.test.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTable/tests/filterFn.test.ts @@ -34,30 +34,30 @@ describe("filterFn", () => { describe("fuzzyContains", () => { const testString = "I am a string with many possible substrings."; - const exactMatch = ["I am", "strin", "ny possi"]; - const fuzzyMatch = ["Iam", "strink", "nt possi"]; - const nonMatch = ["I'm", "strinket", "ny ssi"]; - test("Levenshtein distance 0", () => { - exactMatch.forEach((s) => - expect(ff.fuzzyContains(testString, s, 0)).toBeTruthy() - ); - fuzzyMatch.forEach((s) => - expect(ff.fuzzyContains(testString, s, 0)).toBeFalsy() + test("Short: no typos allowed", () => { + ["i", "am", "a s"].forEach((s) => + expect(ff.fuzzyContains(testString, s)).toBeTruthy() ); - nonMatch.forEach((s) => - expect(ff.fuzzyContains(testString, s, 0)).toBeFalsy() + ["@", "aq"].forEach((s) => + expect(ff.fuzzyContains(testString, s)).toBeFalsy() ); }); - test("Levenshtein distance 1 (default)", () => { - exactMatch.forEach((s) => + test("Medium: 1 typo allowed", () => { + ["i b", "ama", "strim"].forEach((s) => expect(ff.fuzzyContains(testString, s)).toBeTruthy() ); - fuzzyMatch.forEach((s) => + ["i'm", "astrr"].forEach((s) => + expect(ff.fuzzyContains(testString, s)).toBeFalsy() + ); + }); + + test("Long: 2 typos allowed", () => { + ["i'm a string", "with man88"].forEach((s) => expect(ff.fuzzyContains(testString, s)).toBeTruthy() ); - nonMatch.forEach((s) => + ["i'm a ztring", "with man888"].forEach((s) => expect(ff.fuzzyContains(testString, s)).toBeFalsy() ); });