mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
fix(search): place searches find a name typed without its accents (#3075)
"Obidos" did not find "Óbidos", nor "Sao Bras" "São Brás". The search of the places list, a collection, the saved-place picker, the import from a trip and the location picker now compare without case and Latin accents, with letters such as ß, ø or ł folded to their plain spelling. Marks outside Latin script stay, since there they change the word.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// FE-UTIL-PLACESEARCH-001 to FE-UTIL-PLACESEARCH-004
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { placeMatchesSearch } from './placeSearch'
|
||||
import { foldForSearch, placeMatchesSearch, textMatchesSearch } from './placeSearch'
|
||||
|
||||
const place = {
|
||||
name: 'Café Central',
|
||||
@@ -32,3 +32,25 @@ describe('placeMatchesSearch', () => {
|
||||
expect(placeMatchesSearch(place, 'pizza')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('search without accents (#3075)', () => {
|
||||
it('FE-UTIL-PLACESEARCH-005: a query without accents finds the accented name, and the other way round', () => {
|
||||
for (const [query, name] of [['Obidos', 'Óbidos'], ['Evora', 'Évora'], ['Sao Bras', 'São Brás'], ['Loule', 'Loulé']]) {
|
||||
expect(placeMatchesSearch({ name, address: null, description: null, notes: null }, query)).toBe(true)
|
||||
expect(placeMatchesSearch({ name: query, address: null, description: null, notes: null }, name)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('FE-UTIL-PLACESEARCH-006: letters with the mark built in fold to their plain spelling', () => {
|
||||
expect(foldForSearch('Straße')).toBe('strasse')
|
||||
expect(foldForSearch('Łódź')).toBe('lodz')
|
||||
expect(foldForSearch('Ærø')).toBe('aero')
|
||||
expect(textMatchesSearch(['Kraków'], 'krakow')).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-UTIL-PLACESEARCH-007: marks that change a word outside Latin script stay, so が is not か', () => {
|
||||
expect(foldForSearch('が')).toBe('が')
|
||||
expect(textMatchesSearch(['かき'], 'がき')).toBe(false)
|
||||
expect(foldForSearch('한국')).toBe('한국')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,9 +10,33 @@ type Searchable = Pick<Place, 'name' | 'address' | 'description' | 'notes'>
|
||||
* can never disagree about what a search finds.
|
||||
*/
|
||||
export function placeMatchesSearch(place: Searchable, query: string): boolean {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return true
|
||||
return [place.name, place.address, place.description, place.notes].some(
|
||||
field => !!field && field.toLowerCase().includes(q),
|
||||
)
|
||||
return textMatchesSearch([place.name, place.address, place.description, place.notes], query)
|
||||
}
|
||||
|
||||
/** The combining accents of Latin script (U+0300 to U+036F), the marks NFD splits off a letter. */
|
||||
const LATIN_MARKS = new RegExp(`[${String.fromCharCode(0x300)}-${String.fromCharCode(0x36f)}]`, 'g')
|
||||
|
||||
/** Letters that carry their mark in the letter itself, so taking the marks off leaves them as they are. */
|
||||
const FOLDED_LETTERS: Record<string, string> = { ß: 'ss', ø: 'o', æ: 'ae', œ: 'oe', ł: 'l', đ: 'd', ð: 'd', þ: 'th', ı: 'i' }
|
||||
|
||||
/**
|
||||
* Text as a search compares it: lower case and without accents, so "Obidos" finds
|
||||
* "Óbidos", "Sao Bras" finds "São Brás" and the other way round (#3075). Only
|
||||
* Latin marks go: a kana's voicing mark or a Hangul syllable stays whole, since
|
||||
* there it changes the word.
|
||||
*/
|
||||
export function foldForSearch(text: string): string {
|
||||
return text
|
||||
.toLocaleLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(LATIN_MARKS, '')
|
||||
.replace(/[ßøæœłđðþı]/g, ch => FOLDED_LETTERS[ch] ?? ch)
|
||||
.normalize('NFC')
|
||||
}
|
||||
|
||||
/** Whether any of the fields holds the query, accents and case aside. An empty query matches everything. */
|
||||
export function textMatchesSearch(fields: readonly (string | null | undefined)[], query: string): boolean {
|
||||
const q = foldForSearch(query.trim())
|
||||
if (!q) return true
|
||||
return fields.some(field => !!field && foldForSearch(field).includes(q))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user