Add country continent mapping helper

This commit is contained in:
Kari Lintulaakso
2025-10-29 14:49:36 +02:00
parent 9966a2cf3c
commit 517ee2a8b5
3 changed files with 392 additions and 0 deletions
@@ -0,0 +1,347 @@
import { validCountries } from './countryList';
export type Continent =
| 'Africa'
| 'Antarctica'
| 'Asia'
| 'Europe'
| 'North America'
| 'Oceania'
| 'South America';
export type CountryToContinentMap = Record<string, Continent>;
export const countryToContinentMap = {
'Afghanistan': 'Asia',
'Aland Islands': 'Europe',
'Albania': 'Europe',
'Algeria': 'Africa',
'American Samoa': 'Oceania',
'Andorra': 'Europe',
'Angola': 'Africa',
'Anguilla': 'North America',
'Antarctica': 'Antarctica',
'Antigua and Barbuda': 'North America',
'Argentina': 'South America',
'Armenia': 'Asia',
'Aruba': 'North America',
'Ascension Island': 'Africa',
'Ashmore and Cartier Islands': 'Oceania',
'Australia': 'Oceania',
'Austria': 'Europe',
'Azerbaijan': 'Asia',
'Bahamas, The': 'North America',
'Bahrain': 'Asia',
'Baker Island': 'Oceania',
'Bangladesh': 'Asia',
'Barbados': 'North America',
'Bassas Da India': 'Africa',
'Belarus': 'Europe',
'Belgium': 'Europe',
'Belize': 'North America',
'Benin': 'Africa',
'Bermuda': 'North America',
'Bhutan': 'Asia',
'Bolivia': 'South America',
'Bonaire': 'North America',
'Bosnia and Herzegovina': 'Europe',
'Botswana': 'Africa',
'Bouvet Island': 'Antarctica',
'Brazil': 'South America',
'British Indian Ocean Territory': 'Africa',
'British Virgin Islands': 'North America',
'Brunei': 'Asia',
'Bulgaria': 'Europe',
'Burkina Faso': 'Africa',
'Burundi': 'Africa',
'Cambodia': 'Asia',
'Cameroon': 'Africa',
'Canada': 'North America',
'Cape Verde': 'Africa',
'Cayman Islands': 'North America',
'Central African Republic': 'Africa',
'Chad': 'Africa',
'Chile': 'South America',
'China': 'Asia',
'Christmas Island': 'Asia',
'Clipperton Island': 'North America',
'Cocos (Keeling) Islands': 'Asia',
'Colombia': 'South America',
'Comoros': 'Africa',
'Congo, Democratic republic of the (prev. Zaire)': 'Africa',
'Cook Islands': 'Oceania',
'Coral Sea Islands': 'Oceania',
'Costa Rica': 'North America',
"Cote D'Ivoire": 'Africa',
'Croatia': 'Europe',
'Cuba': 'North America',
'Curacao': 'North America',
'Cyprus': 'Asia',
'Czech Republic': 'Europe',
'Denmark': 'Europe',
'Djibouti': 'Africa',
'Dominica': 'North America',
'Dominican Republic': 'North America',
'East Timor': 'Asia',
'Ecuador': 'South America',
'Egypt': 'Africa',
'El Salvador': 'North America',
'Equatorial Guinea': 'Africa',
'Eritrea': 'Africa',
'Estonia': 'Europe',
'Ethiopia': 'Africa',
'Europa Island': 'Africa',
'Falkland Islands (Islas Malvinas)': 'South America',
'Faroe Islands': 'Europe',
'Fiji': 'Oceania',
'Finland': 'Europe',
'France': 'Europe',
'French Guiana': 'South America',
'French Polynesia': 'Oceania',
'French Southern and Antarctic Lands': 'Antarctica',
'Gabon': 'Africa',
'Gambia, The': 'Africa',
'Gaza Strip': 'Asia',
'Georgia': 'Asia',
'Germany': 'Europe',
'Ghana': 'Africa',
'Gibraltar': 'Europe',
'Glorioso Islands': 'Africa',
'Greece': 'Europe',
'Greenland': 'North America',
'Grenada': 'North America',
'Guadeloupe': 'North America',
'Guam': 'Oceania',
'Guatemala': 'North America',
'Guernsey': 'Europe',
'Guinea': 'Africa',
'Guinea-Bissau': 'Africa',
'Guyana': 'South America',
'Haiti': 'North America',
'Heard Island and Mcdonald Islands': 'Antarctica',
'Honduras': 'North America',
'Hong Kong': 'Asia',
'Howland Island': 'Oceania',
'Hungary': 'Europe',
'Iceland': 'Europe',
'India': 'Asia',
'Indonesia': 'Asia',
'Iran': 'Asia',
'Iraq': 'Asia',
'Ireland': 'Europe',
'Isle Of Man': 'Europe',
'Israel': 'Asia',
'Italy': 'Europe',
'Jamaica': 'North America',
'Jan Mayen': 'Europe',
'Japan': 'Asia',
'Jarvis Island': 'North America',
'Jersey': 'Europe',
'Jordan': 'Asia',
'Juan De Nova Island': 'Africa',
'Kazakhstan': 'Asia',
'Kenya': 'Africa',
'Kiribati': 'Oceania',
'Kosovo': 'Europe',
'Kuwait': 'Asia',
'Kyrgyzstan': 'Asia',
'Laos': 'Asia',
'Latvia': 'Europe',
'Lebanon': 'Asia',
'Lesotho': 'Africa',
'Liberia': 'Africa',
'Libya': 'Africa',
'Liechtenstein': 'Europe',
'Lithuania': 'Europe',
'Luxembourg': 'Europe',
'Macau': 'Asia',
'Madagascar': 'Africa',
'Malawi': 'Africa',
'Malaysia': 'Asia',
'Maldives': 'Asia',
'Mali': 'Africa',
'Malta': 'Europe',
'Marshall Islands': 'Oceania',
'Martinique': 'North America',
'Mauritania': 'Africa',
'Mauritius': 'Africa',
'Mayotte': 'Africa',
'Mexico': 'North America',
'Micronesia, Federated States of': 'Oceania',
'Midway Islands': 'Oceania',
'Moldova': 'Europe',
'Monaco': 'Europe',
'Mongolia': 'Asia',
'Montenegro': 'Europe',
'Montserrat': 'North America',
'Morocco': 'Africa',
'Mozambique': 'Africa',
'Myanmar': 'Asia',
'Namibia': 'Africa',
'Nauru': 'Oceania',
'Nepal': 'Asia',
'Netherlands': 'Europe',
'New Caledonia': 'Oceania',
'New Zealand': 'Oceania',
'Nicaragua': 'North America',
'Niger': 'Africa',
'Nigeria': 'Africa',
'Niue': 'Oceania',
"No Man's Land": 'Africa',
'Norfolk Island': 'Oceania',
'North Korea': 'Asia',
'North Macedonia': 'Europe',
'Northern Mariana Islands': 'Oceania',
'Norway': 'Europe',
'Oman': 'Asia',
'Pakistan': 'Asia',
'Palau': 'Oceania',
'Palestine': 'Asia',
'Panama': 'North America',
'Papua New Guinea': 'Oceania',
'Paracel Islands': 'Asia',
'Paraguay': 'South America',
'Peru': 'South America',
'Philippines': 'Asia',
'Pitcairn Islands': 'Oceania',
'Poland': 'Europe',
'Portugal': 'Europe',
'Puerto Rico': 'North America',
'Qatar': 'Asia',
'Reunion': 'Africa',
'Romania': 'Europe',
'Russia': 'Europe',
'Rwanda': 'Africa',
'Saba': 'North America',
'Saint Barthelemy': 'North America',
'Saint Eustatius': 'North America',
'Saint Helena': 'Africa',
'Saint Kitts and Nevis': 'North America',
'Saint Lucia': 'North America',
'Saint Martin': 'North America',
'Saint Pierre and Miquelon': 'North America',
'Saint Vincent and the Grenadines': 'North America',
'Samoa': 'Oceania',
'San Marino': 'Europe',
'Sao Tome and Principe': 'Africa',
'Saudi Arabia': 'Asia',
'Senegal': 'Africa',
'Serbia': 'Europe',
'Seychelles': 'Africa',
'Sierra Leone': 'Africa',
'Singapore': 'Asia',
'Slovakia': 'Europe',
'Slovenia': 'Europe',
'Solomon Islands': 'Oceania',
'Somalia': 'Africa',
'South Africa': 'Africa',
'South Georgia and the South Sandwich Islands': 'Antarctica',
'South Korea': 'Asia',
'South Sudan': 'Africa',
'Spain': 'Europe',
'Spratly Islands': 'Asia',
'Sri Lanka': 'Asia',
'Sudan': 'Africa',
'Suriname': 'South America',
'Svalbard': 'Europe',
'Swaziland': 'Africa',
'Sweden': 'Europe',
'Switzerland': 'Europe',
'Syria': 'Asia',
'Taiwan': 'Asia',
'Tajikistan': 'Asia',
'Tanzania': 'Africa',
'Thailand': 'Asia',
'Togo': 'Africa',
'Tokelau': 'Oceania',
'Tonga': 'Oceania',
'Trinidad and Tobago': 'North America',
'Tromelin Island': 'Africa',
'Tunisia': 'Africa',
'Turkey': 'Asia',
'Turkmenistan': 'Asia',
'Turks and Caicos Islands': 'North America',
'Tuvalu': 'Oceania',
'Uganda': 'Africa',
'Ukraine': 'Europe',
'United Arab Emirates': 'Asia',
'United Kingdom': 'Europe',
'United States Minor Outlying Islands': 'Oceania',
'United States of America': 'North America',
'Uruguay': 'South America',
'Uzbekistan': 'Asia',
'Vanuatu': 'Oceania',
'Vatican City': 'Europe',
'Venezuela': 'South America',
'Vietnam': 'Asia',
'Virgin Islands': 'North America',
'Wake Island': 'Oceania',
'Wallis and Futuna': 'Oceania',
'West Bank': 'Asia',
'Western Sahara': 'Africa',
'Yemen': 'Asia',
'Zambia': 'Africa',
'Zimbabwe': 'Africa',
} as const satisfies CountryToContinentMap;
const normalizedCountryLookup = new Map<string, string>(
validCountries.map(country => [country.toLowerCase(), country]),
);
const continentAliases: Record<Continent, string[]> = {
Africa: ['africa'],
Antarctica: ['antarctica'],
Asia: ['asia'],
Europe: ['europe'],
'North America': [
'north america',
'northern america',
'central america',
'caribbean',
'america',
'americas',
],
Oceania: ['oceania', 'australia', 'australasia', 'pacific'],
'South America': ['south america', 'southern america', 'latin america', 'america', 'americas'],
};
const normalize = (value: string | null | undefined) => value?.trim().toLowerCase() ?? '';
export const getContinentByCountry = (
country: string | null | undefined,
): Continent | undefined => {
const normalizedCountry = normalize(country);
if (!normalizedCountry) return undefined;
const canonicalCountry = normalizedCountryLookup.get(normalizedCountry);
if (!canonicalCountry) return undefined;
return countryToContinentMap[canonicalCountry as keyof typeof countryToContinentMap];
};
export const matchesCountryOrContinent = (
country: string | null | undefined,
filterValue: string | null | undefined,
): boolean => {
const normalizedFilter = normalize(filterValue);
if (!normalizedFilter) return true;
const normalizedCountry = normalize(country);
if (normalizedCountry.includes(normalizedFilter)) {
return true;
}
const continent = getContinentByCountry(country);
if (!continent) {
return false;
}
const normalizedContinent = continent.toLowerCase();
if (normalizedContinent.includes(normalizedFilter)) {
return true;
}
return continentAliases[continent].some(
alias => alias.includes(normalizedFilter) || normalizedFilter.includes(alias),
);
};
@@ -272,3 +272,7 @@ export const validCountries = [
'Zambia',
'Zimbabwe',
]
export const validCountrySet = new Set(validCountries)
export const isValidCountry = (value: string): boolean => validCountrySet.has(value)
@@ -0,0 +1,41 @@
import { describe, expect, it } from '@jest/globals'
import { validCountries } from '../../shared/validators/countryList'
import {
countryToContinentMap,
getContinentByCountry,
matchesCountryOrContinent,
} from '../../shared/validators/countryContinents'
describe('country continent mapping', () => {
it('maps every valid country to a continent', () => {
const missingCountries = validCountries.filter(
country => !(country in countryToContinentMap)
)
expect(missingCountries).toHaveLength(0)
})
it('resolves continents case-insensitively', () => {
expect(getContinentByCountry(' canada ')).toBe('North America')
expect(getContinentByCountry('BRAZIL')).toBe('South America')
expect(getContinentByCountry('unknown')).toBeUndefined()
})
it('matches continent aliases and country names', () => {
expect(matchesCountryOrContinent('Finland', 'Europe')).toBe(true)
expect(matchesCountryOrContinent('Finland', 'eur')).toBe(true)
expect(matchesCountryOrContinent('Finland', 'asia')).toBe(false)
expect(matchesCountryOrContinent('Mexico', 'central america')).toBe(true)
expect(matchesCountryOrContinent('Brazil', 'latin america')).toBe(true)
expect(matchesCountryOrContinent('Canada', 'americas')).toBe(true)
expect(matchesCountryOrContinent('Kenya', 'afr')).toBe(true)
expect(matchesCountryOrContinent('Australia', 'oceania')).toBe(true)
expect(matchesCountryOrContinent('Australia', 'pacific')).toBe(true)
expect(matchesCountryOrContinent('Kenya', 'oceania')).toBe(false)
expect(matchesCountryOrContinent(null, 'Europe')).toBe(false)
expect(matchesCountryOrContinent('Kenya', '')).toBe(true)
})
})