Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved country search (v2) #35408

Merged
merged 11 commits into from
Feb 13, 2024
44 changes: 40 additions & 4 deletions src/libs/searchCountryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,54 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[])
if (!trimmedSearchValue) {
return [];
}

const filteredData = countriesData.filter((country) => country.searchValue.includes(trimmedSearchValue));

return filteredData.sort((a, b) => {
if (a.value.toLowerCase() === trimmedSearchValue) {
const halfSorted = filteredData.sort((a, b) => {
// Prioritize matches at the beginning of the string
// e.g. For the search term "Bar" "Barbados" should be prioritized over Antigua & Barbuda
// The first two characters are the country code, so we start at index 2
// and end at the length of the search term
const countryNameASubstring = a.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2);
const countryNameBSubstring = b.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2);
if (countryNameASubstring === trimmedSearchValue.toLowerCase()) {
return -1;
}
if (b.value.toLowerCase() === trimmedSearchValue) {
if (countryNameBSubstring === trimmedSearchValue.toLowerCase()) {
return 1;
}
return 0;
});

let fullSorted;
const unsanitizedSearchValue = searchValue.toLowerCase().trim();
if (trimmedSearchValue !== unsanitizedSearchValue) {
// Diacritic detected, prioritize diacritic matches
// We search for diacritic matches by using the unsanitized country name and search term
fullSorted = halfSorted.sort((a, b) => {
const unsanitizedCountryNameA = a.text.toLowerCase();
const unsanitizedCountryNameB = b.text.toLowerCase();
if (unsanitizedCountryNameA.includes(unsanitizedSearchValue)) {
return -1;
}
if (unsanitizedCountryNameB.includes(unsanitizedSearchValue)) {
return 1;
}
return 0;
});
} else {
// Diacritic not detected, prioritize country code matches (country codes can never contain diacritics)
// E.g. the search term 'US' should push 'United States' to the top
fullSorted = halfSorted.sort((a, b) => {
if (a.value.toLowerCase() === trimmedSearchValue) {
return -1;
}
if (b.value.toLowerCase() === trimmedSearchValue) {
return 1;
}
return 0;
});
}
return fullSorted;
}

export default searchCountryOptions;
Expand Down
281 changes: 281 additions & 0 deletions tests/unit/searchCountryOptionsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import searchCountryOptions from '@libs/searchCountryOptions';

describe('searchCountryOptions', () => {
test('when the search term is a country code, the country with that code should be prioritized', () => {
const searchValue = 'US';
const countriesData = [
{
value: 'US',
keyForList: 'US',
text: 'United States',
isSelected: false,
searchValue: 'usunitedstates',
},
{
value: 'CA',
keyForList: 'CA',
text: 'Canada',
isSelected: false,
searchValue: 'cacanada',
},
{
value: 'MX',
keyForList: 'MX',
text: 'Mexico',
isSelected: false,
searchValue: 'mxmexico',
},
{
value: 'AU',
keyForList: 'AU',
text: 'Australia',
isSelected: false,
searchValue: 'auaustralia',
},
];
const expected = [
{
value: 'US',
keyForList: 'US',
text: 'United States',
isSelected: false,
searchValue: 'usunitedstates',
},
{
value: 'AU',
keyForList: 'AU',
text: 'Australia',
isSelected: false,
searchValue: 'auaustralia',
},
];
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
test('when the search term contains diacritics the country names that exactly match should be prioritized', () => {
const searchValue = 'Ål';
const countriesData = [
{
value: 'AX',
keyForList: 'AX',
text: 'Åland Islands',
isSelected: false,
searchValue: 'axalandislands',
},
{
value: 'AL',
keyForList: 'AL',
text: 'Albania',
isSelected: false,
searchValue: 'alalbania',
},
{
value: 'AS',
keyForList: 'AS',
text: 'American Samoa',
isSelected: false,
searchValue: 'asamericansamoa',
},
];
const expected = [
{
value: 'AX',
keyForList: 'AX',
text: 'Åland Islands',
isSelected: false,
searchValue: 'axalandislands',
},
{
value: 'AL',
keyForList: 'AL',
text: 'Albania',
isSelected: false,
searchValue: 'alalbania',
},
];
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
test('when the search term contains diacritics the country names that exactly match should be prioritized, test case #2', () => {
const searchValue = 'é';
const countriesData = [
{
value: 'BE',
keyForList: 'BE',
text: 'Belgium',
isSelected: false,
searchValue: 'bebelgium',
},
{
value: 'US',
keyForList: 'US',
text: 'United States',
isSelected: false,
searchValue: 'usunitedstates',
},
{
value: 'BL',
keyForList: 'BL',
text: 'Saint Barthélemy',
isSelected: false,
searchValue: 'blsaintbarthelemy',
},
];
const expected = [
{
value: 'BL',
keyForList: 'BL',
text: 'Saint Barthélemy',
isSelected: false,
searchValue: 'blsaintbarthelemy',
},
{
value: 'BE',
keyForList: 'BE',
text: 'Belgium',
isSelected: false,
searchValue: 'bebelgium',
},
{
value: 'US',
keyForList: 'US',
text: 'United States',
isSelected: false,
searchValue: 'usunitedstates',
},
];
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
test('when the search term contains no diacritics, countries with diacritics should still be searched by their sanitized names', () => {
const searchValue = 'al';
const countriesData = [
{
value: 'AX',
keyForList: 'AX',
text: 'Åland Islands',
isSelected: false,
searchValue: 'axalandislands',
},
{
value: 'AL',
keyForList: 'AL',
text: 'Albania',
isSelected: false,
searchValue: 'alalbania',
},
{
value: 'AS',
keyForList: 'AS',
text: 'American Samoa',
isSelected: false,
searchValue: 'asamericansamoa',
},
];
const expected = [
{
value: 'AL',
keyForList: 'AL',
text: 'Albania',
isSelected: false,
searchValue: 'alalbania',
},
{
value: 'AX',
keyForList: 'AX',
text: 'Åland Islands',
isSelected: false,
searchValue: 'axalandislands',
},
];
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
test('when a search term exactly matches the beginning of a countries name, that country should be prioritized', () => {
const searchValue = 'bar'; // for barbados
const countriesData = [
{
value: 'BB',
keyForList: 'BB',
text: 'Barbados',
isSelected: false,
searchValue: 'bbbarbados',
},
{
value: 'BY',
keyForList: 'BY',
text: 'Belarus',
isSelected: false,
searchValue: 'bybelarus',
},
{
value: 'BE',
keyForList: 'BE',
text: 'Belgium',
isSelected: false,
searchValue: 'bebelgium',
},
{
value: 'AG',
keyForList: 'AG',
text: 'Antigua and Barbuda',
isSelected: false,
searchValue: 'agantiguaandbarbuda',
},
];
const expected = [
{
value: 'BB',
keyForList: 'BB',
text: 'Barbados',
isSelected: false,
searchValue: 'bbbarbados',
},
{
value: 'AG',
keyForList: 'AG',
text: 'Antigua and Barbuda',
isSelected: false,
searchValue: 'agantiguaandbarbuda',
},
];
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
test('when the search term is empty, all countries should be returned', () => {
const searchValue = '';
const countriesData = [
{
value: 'BB',
keyForList: 'BB',
text: 'Barbados',
isSelected: false,
searchValue: 'bbbarbados',
},
{
value: 'BY',
keyForList: 'BY',
text: 'Belarus',
isSelected: false,
searchValue: 'bybelarus',
},
{
value: 'BE',
keyForList: 'BE',
text: 'Belgium',
isSelected: false,
searchValue: 'bebelgium',
},
{
value: 'AG',
keyForList: 'AG',
text: 'Antigua and Barbuda',
isSelected: false,
searchValue: 'agantiguaandbarbuda',
},
];
const expected = countriesData;
const actual = searchCountryOptions(searchValue, countriesData);
expect(actual).toEqual(expected);
});
});
Loading