SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
13.3 KB · 291 lines typescript
Raw Blame History
1/**2 * ClinicalTrials.gov country names → ISO 3166-1 alpha-3 (pure, no I/O).3 *4 * ClinicalTrials.gov stores the country of a location as free text chosen by the registrant from5 * its own list; the API v2 returns mostly short English names ("United States", "South Korea",6 * "Turkey (Türkiye)", "Reunion") while older records and the legacy API used ISO-style long names7 * ("Korea, Republic of", "Russian Federation", "Réunion", "Viet Nam"). Both families are accepted8 * here. Territories keep their own ISO code (Puerto Rico → PRI, Hong Kong → HKG, Réunion → REU…)9 * because that is how the registrant counted the site; the UI may roll them up if it wishes.10 *11 * Dissolved states that have no ISO 3166-1 code today are deliberately left unmapped and listed in12 * `UNMAPPED_COUNTRY_NAMES` so a test fails when a new name appears in the database without a decision.13 */1415export interface CountryCode {16  /** ISO 3166-1 alpha-3 (Kosovo uses the user-assigned XKX, as the World Bank and EU do). */17  iso3: string;18  /** Display name (short English form). */19  name: string;20  /** Spellings seen in ClinicalTrials.gov exports, in addition to `name`. */21  aliases?: string[];22}2324export const COUNTRY_CODES: readonly CountryCode[] = [25  { iso3: 'AFG', name: 'Afghanistan' },26  { iso3: 'ALB', name: 'Albania' },27  { iso3: 'DZA', name: 'Algeria' },28  { iso3: 'ASM', name: 'American Samoa' },29  { iso3: 'AND', name: 'Andorra' },30  { iso3: 'AGO', name: 'Angola' },31  { iso3: 'ATG', name: 'Antigua and Barbuda' },32  { iso3: 'ARG', name: 'Argentina' },33  { iso3: 'ARM', name: 'Armenia' },34  { iso3: 'ABW', name: 'Aruba' },35  { iso3: 'AUS', name: 'Australia' },36  { iso3: 'AUT', name: 'Austria' },37  { iso3: 'AZE', name: 'Azerbaijan' },38  { iso3: 'BHS', name: 'Bahamas', aliases: ['The Bahamas', 'Bahamas, The'] },39  { iso3: 'BHR', name: 'Bahrain' },40  { iso3: 'BGD', name: 'Bangladesh' },41  { iso3: 'BRB', name: 'Barbados' },42  { iso3: 'BLR', name: 'Belarus' },43  { iso3: 'BEL', name: 'Belgium' },44  { iso3: 'BLZ', name: 'Belize' },45  { iso3: 'BEN', name: 'Benin' },46  { iso3: 'BMU', name: 'Bermuda' },47  { iso3: 'BTN', name: 'Bhutan' },48  { iso3: 'BOL', name: 'Bolivia', aliases: ['Bolivia, Plurinational State of', 'Bolivia (Plurinational State of)'] },49  { iso3: 'BIH', name: 'Bosnia and Herzegovina' },50  { iso3: 'BWA', name: 'Botswana' },51  { iso3: 'BRA', name: 'Brazil' },52  { iso3: 'BRN', name: 'Brunei', aliases: ['Brunei Darussalam'] },53  { iso3: 'BGR', name: 'Bulgaria' },54  { iso3: 'BFA', name: 'Burkina Faso' },55  { iso3: 'BDI', name: 'Burundi' },56  { iso3: 'KHM', name: 'Cambodia' },57  { iso3: 'CMR', name: 'Cameroon' },58  { iso3: 'CAN', name: 'Canada' },59  { iso3: 'CPV', name: 'Cape Verde', aliases: ['Cabo Verde'] },60  { iso3: 'CYM', name: 'Cayman Islands' },61  { iso3: 'CAF', name: 'Central African Republic' },62  { iso3: 'TCD', name: 'Chad' },63  { iso3: 'CHL', name: 'Chile' },64  { iso3: 'CHN', name: 'China' },65  { iso3: 'COL', name: 'Colombia' },66  { iso3: 'COM', name: 'Comoros' },67  { iso3: 'COG', name: 'Congo', aliases: ['Republic of the Congo', 'Congo, Republic of the'] },68  { iso3: 'COD', name: 'Democratic Republic of the Congo', aliases: ['Congo, The Democratic Republic of the', 'Congo, Democratic Republic of the', 'Congo, The Democratic Republic'] },69  { iso3: 'CRI', name: 'Costa Rica' },70  { iso3: 'CIV', name: "Côte d'Ivoire", aliases: ["Cote d'Ivoire", 'Ivory Coast'] },71  { iso3: 'HRV', name: 'Croatia' },72  { iso3: 'CUB', name: 'Cuba' },73  { iso3: 'CUW', name: 'Curaçao', aliases: ['Curacao'] },74  { iso3: 'CYP', name: 'Cyprus' },75  { iso3: 'CZE', name: 'Czechia', aliases: ['Czech Republic'] },76  { iso3: 'DNK', name: 'Denmark' },77  { iso3: 'DJI', name: 'Djibouti' },78  { iso3: 'DMA', name: 'Dominica' },79  { iso3: 'DOM', name: 'Dominican Republic' },80  { iso3: 'ECU', name: 'Ecuador' },81  { iso3: 'EGY', name: 'Egypt' },82  { iso3: 'SLV', name: 'El Salvador' },83  { iso3: 'GNQ', name: 'Equatorial Guinea' },84  { iso3: 'ERI', name: 'Eritrea' },85  { iso3: 'EST', name: 'Estonia' },86  { iso3: 'SWZ', name: 'Eswatini', aliases: ['Swaziland'] },87  { iso3: 'ETH', name: 'Ethiopia' },88  { iso3: 'FRO', name: 'Faroe Islands', aliases: ['Faeroe Islands'] },89  { iso3: 'FJI', name: 'Fiji' },90  { iso3: 'FIN', name: 'Finland' },91  { iso3: 'FRA', name: 'France' },92  { iso3: 'GUF', name: 'French Guiana' },93  { iso3: 'PYF', name: 'French Polynesia' },94  { iso3: 'GAB', name: 'Gabon' },95  { iso3: 'GMB', name: 'Gambia', aliases: ['The Gambia', 'Gambia, The'] },96  { iso3: 'GEO', name: 'Georgia' },97  { iso3: 'DEU', name: 'Germany' },98  { iso3: 'GHA', name: 'Ghana' },99  { iso3: 'GIB', name: 'Gibraltar' },100  { iso3: 'GRC', name: 'Greece' },101  { iso3: 'GRL', name: 'Greenland' },102  { iso3: 'GRD', name: 'Grenada' },103  { iso3: 'GLP', name: 'Guadeloupe' },104  { iso3: 'GUM', name: 'Guam' },105  { iso3: 'GTM', name: 'Guatemala' },106  { iso3: 'GIN', name: 'Guinea' },107  { iso3: 'GNB', name: 'Guinea-Bissau' },108  { iso3: 'GUY', name: 'Guyana' },109  { iso3: 'HTI', name: 'Haiti' },110  { iso3: 'HND', name: 'Honduras' },111  { iso3: 'HKG', name: 'Hong Kong', aliases: ['Hong Kong SAR', 'Hong Kong, China'] },112  { iso3: 'HUN', name: 'Hungary' },113  { iso3: 'ISL', name: 'Iceland' },114  { iso3: 'IND', name: 'India' },115  { iso3: 'IDN', name: 'Indonesia' },116  { iso3: 'IRN', name: 'Iran', aliases: ['Iran, Islamic Republic of', 'Iran (Islamic Republic of)'] },117  { iso3: 'IRQ', name: 'Iraq' },118  { iso3: 'IRL', name: 'Ireland' },119  { iso3: 'ISR', name: 'Israel' },120  { iso3: 'ITA', name: 'Italy' },121  { iso3: 'JAM', name: 'Jamaica' },122  { iso3: 'JPN', name: 'Japan' },123  { iso3: 'JOR', name: 'Jordan' },124  { iso3: 'KAZ', name: 'Kazakhstan' },125  { iso3: 'KEN', name: 'Kenya' },126  { iso3: 'PRK', name: 'North Korea', aliases: ["Korea, Democratic People's Republic of", "Democratic People's Republic of Korea"] },127  { iso3: 'KOR', name: 'South Korea', aliases: ['Korea, Republic of', 'Republic of Korea', 'Korea'] },128  { iso3: 'XKX', name: 'Kosovo' },129  { iso3: 'KWT', name: 'Kuwait' },130  { iso3: 'KGZ', name: 'Kyrgyzstan' },131  { iso3: 'LAO', name: 'Laos', aliases: ["Lao People's Democratic Republic"] },132  { iso3: 'LVA', name: 'Latvia' },133  { iso3: 'LBN', name: 'Lebanon' },134  { iso3: 'LSO', name: 'Lesotho' },135  { iso3: 'LBR', name: 'Liberia' },136  { iso3: 'LBY', name: 'Libya', aliases: ['Libyan Arab Jamahiriya'] },137  { iso3: 'LIE', name: 'Liechtenstein' },138  { iso3: 'LTU', name: 'Lithuania' },139  { iso3: 'LUX', name: 'Luxembourg' },140  { iso3: 'MAC', name: 'Macao', aliases: ['Macau', 'Macao SAR'] },141  { iso3: 'MDG', name: 'Madagascar' },142  { iso3: 'MWI', name: 'Malawi' },143  { iso3: 'MYS', name: 'Malaysia' },144  { iso3: 'MDV', name: 'Maldives' },145  { iso3: 'MLI', name: 'Mali' },146  { iso3: 'MLT', name: 'Malta' },147  { iso3: 'MTQ', name: 'Martinique' },148  { iso3: 'MRT', name: 'Mauritania' },149  { iso3: 'MUS', name: 'Mauritius' },150  { iso3: 'MYT', name: 'Mayotte' },151  { iso3: 'MEX', name: 'Mexico' },152  { iso3: 'MDA', name: 'Moldova', aliases: ['Moldova, Republic of', 'Republic of Moldova'] },153  { iso3: 'MCO', name: 'Monaco' },154  { iso3: 'MNG', name: 'Mongolia' },155  { iso3: 'MNE', name: 'Montenegro' },156  { iso3: 'MAR', name: 'Morocco' },157  { iso3: 'MOZ', name: 'Mozambique' },158  { iso3: 'MMR', name: 'Myanmar', aliases: ['Burma'] },159  { iso3: 'NAM', name: 'Namibia' },160  { iso3: 'NPL', name: 'Nepal' },161  { iso3: 'NLD', name: 'Netherlands', aliases: ['The Netherlands', 'Netherlands, The'] },162  { iso3: 'NCL', name: 'New Caledonia' },163  { iso3: 'NZL', name: 'New Zealand' },164  { iso3: 'NIC', name: 'Nicaragua' },165  { iso3: 'NER', name: 'Niger' },166  { iso3: 'NGA', name: 'Nigeria' },167  { iso3: 'MKD', name: 'North Macedonia', aliases: ['Macedonia, The Former Yugoslav Republic of', 'Macedonia', 'North Macedonia, Republic of'] },168  { iso3: 'MNP', name: 'Northern Mariana Islands' },169  { iso3: 'NOR', name: 'Norway' },170  { iso3: 'OMN', name: 'Oman' },171  { iso3: 'PAK', name: 'Pakistan' },172  { iso3: 'PSE', name: 'Palestinian Territories', aliases: ['Palestinian Territory, occupied', 'Palestinian Territory', 'Palestine, State of', 'State of Palestine', 'Palestine'] },173  { iso3: 'PAN', name: 'Panama' },174  { iso3: 'PNG', name: 'Papua New Guinea' },175  { iso3: 'PRY', name: 'Paraguay' },176  { iso3: 'PER', name: 'Peru' },177  { iso3: 'PHL', name: 'Philippines' },178  { iso3: 'POL', name: 'Poland' },179  { iso3: 'PRT', name: 'Portugal' },180  { iso3: 'PRI', name: 'Puerto Rico' },181  { iso3: 'QAT', name: 'Qatar' },182  { iso3: 'REU', name: 'Réunion', aliases: ['Reunion'] },183  { iso3: 'ROU', name: 'Romania' },184  { iso3: 'RUS', name: 'Russia', aliases: ['Russian Federation'] },185  { iso3: 'RWA', name: 'Rwanda' },186  { iso3: 'KNA', name: 'Saint Kitts and Nevis' },187  { iso3: 'LCA', name: 'Saint Lucia' },188  { iso3: 'VCT', name: 'Saint Vincent and the Grenadines' },189  { iso3: 'WSM', name: 'Samoa' },190  { iso3: 'SMR', name: 'San Marino' },191  { iso3: 'STP', name: 'São Tomé and Príncipe', aliases: ['Sao Tome and Principe'] },192  { iso3: 'SAU', name: 'Saudi Arabia' },193  { iso3: 'SEN', name: 'Senegal' },194  { iso3: 'SRB', name: 'Serbia' },195  { iso3: 'SYC', name: 'Seychelles' },196  { iso3: 'SLE', name: 'Sierra Leone' },197  { iso3: 'SGP', name: 'Singapore' },198  { iso3: 'SVK', name: 'Slovakia' },199  { iso3: 'SVN', name: 'Slovenia' },200  { iso3: 'SLB', name: 'Solomon Islands' },201  { iso3: 'SOM', name: 'Somalia' },202  { iso3: 'ZAF', name: 'South Africa' },203  { iso3: 'SSD', name: 'South Sudan' },204  { iso3: 'ESP', name: 'Spain' },205  { iso3: 'LKA', name: 'Sri Lanka' },206  { iso3: 'SDN', name: 'Sudan' },207  { iso3: 'SUR', name: 'Suriname' },208  { iso3: 'SWE', name: 'Sweden' },209  { iso3: 'CHE', name: 'Switzerland' },210  { iso3: 'SYR', name: 'Syria', aliases: ['Syrian Arab Republic'] },211  { iso3: 'TWN', name: 'Taiwan', aliases: ['Taiwan, Province of China'] },212  { iso3: 'TJK', name: 'Tajikistan' },213  { iso3: 'TZA', name: 'Tanzania', aliases: ['Tanzania, United Republic of', 'United Republic of Tanzania'] },214  { iso3: 'THA', name: 'Thailand' },215  { iso3: 'TLS', name: 'Timor-Leste', aliases: ['East Timor'] },216  { iso3: 'TGO', name: 'Togo' },217  { iso3: 'TON', name: 'Tonga' },218  { iso3: 'TTO', name: 'Trinidad and Tobago' },219  { iso3: 'TUN', name: 'Tunisia' },220  { iso3: 'TUR', name: 'Türkiye', aliases: ['Turkey (Türkiye)', 'Turkey', 'Turkiye'] },221  { iso3: 'TKM', name: 'Turkmenistan' },222  { iso3: 'UGA', name: 'Uganda' },223  { iso3: 'UKR', name: 'Ukraine' },224  { iso3: 'ARE', name: 'United Arab Emirates' },225  { iso3: 'GBR', name: 'United Kingdom', aliases: ['United Kingdom of Great Britain and Northern Ireland', 'UK', 'Great Britain'] },226  { iso3: 'USA', name: 'United States', aliases: ['United States of America', 'USA'] },227  { iso3: 'URY', name: 'Uruguay' },228  { iso3: 'UZB', name: 'Uzbekistan' },229  { iso3: 'VUT', name: 'Vanuatu' },230  { iso3: 'VEN', name: 'Venezuela', aliases: ['Venezuela, Bolivarian Republic of', 'Venezuela (Bolivarian Republic of)'] },231  { iso3: 'VNM', name: 'Vietnam', aliases: ['Viet Nam'] },232  // ClinicalTrials.gov writes "Virgin Islands" for sites in the U.S. Virgin Islands (facilities in St. Thomas / St. Croix); the British Virgin Islands are spelled out.233  { iso3: 'VIR', name: 'U.S. Virgin Islands', aliases: ['Virgin Islands', 'Virgin Islands (U.S.)', 'Virgin Islands, U.S.', 'United States Virgin Islands'] },234  { iso3: 'VGB', name: 'British Virgin Islands', aliases: ['Virgin Islands, British'] },235  { iso3: 'YEM', name: 'Yemen' },236  { iso3: 'ZMB', name: 'Zambia' },237  { iso3: 'ZWE', name: 'Zimbabwe' },238];239240/**241 * Names present in ClinicalTrials.gov exports that have NO current ISO 3166-1 code (dissolved242 * states or an empty field). They keep `iso3 = null` in `trial_site_country_counts`, appear in the243 * table by name, and are never painted on the map.244 */245export const UNMAPPED_COUNTRY_NAMES: readonly string[] = ['', 'Serbia and Montenegro', 'Former Serbia and Montenegro', 'Federal Republic of Yugoslavia', 'Former Yugoslavia', 'Yugoslavia', 'Netherlands Antilles', 'Czechoslovakia', 'Union of Soviet Socialist Republics', 'USSR'];246247/** Lower-case, trimmed, straight apostrophes, collapsed whitespace. */248export function normalizeCountryName(name: string): string {249  return name250    .normalize('NFC')251    .replace(/[‘’ʼ`´]/g, "'")252    .replace(/\s+/g, ' ')253    .trim()254    .toLowerCase();255}256257const NAME_INDEX: ReadonlyMap<string, CountryCode> = (() => {258  const m = new Map<string, CountryCode>();259  for (const c of COUNTRY_CODES) {260    m.set(normalizeCountryName(c.name), c);261    for (const a of c.aliases ?? []) m.set(normalizeCountryName(a), c);262    m.set(c.iso3.toLowerCase(), c);263  }264  return m;265})();266267const ISO_INDEX: ReadonlyMap<string, CountryCode> = new Map(COUNTRY_CODES.map((c) => [c.iso3, c]));268269/** ISO 3166-1 alpha-3 for a ClinicalTrials.gov country name, or null when unknown/unmappable. */270export function countryToIso3(name: string | null | undefined): string | null {271  if (name == null) return null;272  const c = NAME_INDEX.get(normalizeCountryName(name));273  return c ? c.iso3 : null;274}275276/** Full entry (iso3 + display name) for a ClinicalTrials.gov country name or an ISO3 code. */277export function lookupCountry(nameOrIso3: string | null | undefined): CountryCode | null {278  if (nameOrIso3 == null) return null;279  return NAME_INDEX.get(normalizeCountryName(nameOrIso3)) ?? null;280}281282/** Display name for an ISO3 code (falls back to the code). */283export function iso3ToName(iso3: string): string {284  return ISO_INDEX.get(iso3.toUpperCase())?.name ?? iso3;285}286287export function isKnownUnmapped(name: string): boolean {288  const n = normalizeCountryName(name);289  return UNMAPPED_COUNTRY_NAMES.some((u) => normalizeCountryName(u) === n);290}291