'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'ā' => 'a', 'ă' => 'a', 'ą' => 'a', 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ď' => 'd', 'đ' => 'd', 'ð' => 'd', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ē' => 'e', 'ė' => 'e', 'ę' => 'e', 'ě' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 'ı' => 'i', 'ł' => 'l', 'ñ' => 'n', 'ń' => 'n', 'ň' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ō' => 'o', 'ő' => 'o', 'ř' => 'r', 'ś' => 's', 'š' => 's', 'ş' => 's', 'ș' => 's', 'ť' => 't', 'ț' => 't', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ū' => 'u', 'ů' => 'u', 'ű' => 'u', 'ý' => 'y', 'ÿ' => 'y', 'ž' => 'z', 'ź' => 'z', 'ż' => 'z', 'ß' => 'ss', 'æ' => 'ae', 'œ' => 'oe', 'þ' => 'th', ]; /** * Alternative names, already in normalized form (see normalize()). If a * country's name is in a group, every name in the group is accepted. * Only names that belong to exactly one country belong here: "congo" and * "korea" are left out on purpose. */ private static $aliasGroups = [ ['united states of america', 'united states', 'usa', 'u s a', 'us', 'u s', 'america'], ['united kingdom', 'uk', 'u k', 'great britain', 'britain'], ['czechia', 'czech republic'], ['myanmar', 'burma'], ['timor leste', 'east timor'], ['cote d ivoire', 'ivory coast'], ['eswatini', 'swaziland'], ['north macedonia', 'macedonia'], ['cabo verde', 'cape verde'], ['turkey', 'turkiye'], ['netherlands', 'holland'], ['democratic republic of the congo', 'democratic republic of congo', 'dr congo', 'drc', 'congo kinshasa'], ['republic of the congo', 'republic of congo', 'congo brazzaville'], ['south korea', 'republic of korea', 'korea south'], ['north korea', 'democratic peoples republic of korea', 'korea north', 'dprk'], ['russia', 'russian federation'], ['vatican city', 'vatican', 'holy see', 'vatican city state'], ['micronesia', 'federated states of micronesia'], ['brunei', 'brunei darussalam'], ['laos', 'lao peoples democratic republic'], ['syria', 'syrian arab republic'], ['vietnam', 'viet nam'], ['iran', 'islamic republic of iran'], ['bolivia', 'plurinational state of bolivia'], ['venezuela', 'bolivarian republic of venezuela'], ['tanzania', 'united republic of tanzania'], ['moldova', 'republic of moldova'], ['palestine', 'state of palestine'], ['kosovo', 'republic of kosovo'], ['sao tome and principe', 'sao tome'], ['trinidad and tobago', 'trinidad'], ['antigua and barbuda', 'antigua'], ['bosnia and herzegovina', 'bosnia', 'herzegovina'], ['united arab emirates', 'uae'], ['saint kitts and nevis', 'saint kitts'], ['saint vincent and the grenadines', 'saint vincent'], ]; /** * Lower case, accents removed, punctuation turned into single spaces. */ public static function normalize($name) { // e107 stores an apostrophe as ' ("Cote d'Ivoire"), so decode first. $s = html_entity_decode((string) $name, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $s = mb_strtolower($s, 'UTF-8'); $s = strtr($s, self::$translit); $s = str_replace('&', ' and ', $s); $s = preg_replace('/[^a-z0-9]+/', ' ', $s); $s = trim(preg_replace('/\s+/', ' ', $s)); $s = preg_replace('/^the /', '', $s); $s = preg_replace('/\bst\b/', 'saint', $s); return $s; } /** * Every normalized form accepted for a country name. * * @return string[] */ public static function forms($name) { $name = (string) $name; $variants = [$name]; if (preg_match('/^(.*?)\s*\((.*)\)\s*$/u', $name, $m)) { $variants[] = $m[1]; $variants[] = $m[2]; } foreach ($variants as $variant) { if (strpos($variant, ',') !== false) { $parts = array_map('trim', explode(',', $variant, 2)); $variants[] = $parts[1] . ' ' . $parts[0]; // "Congo, Republic of the" -> "Republic of the Congo" } } $forms = []; foreach ($variants as $variant) { $form = self::normalize($variant); if ($form !== '') { $forms[$form] = true; } } foreach (array_keys($forms) as $form) { foreach (self::$aliasGroups as $group) { if (in_array((string) $form, $group, true)) { foreach ($group as $member) { $forms[$member] = true; } } } } return array_map('strval', array_keys($forms)); } /** * Is $typed a correct name for the country called $expected? * * @param string $expected The country's name as stored (tk_countries.country). * @param string $typed What the student typed. * @param string[] $allNames Every country name, so another country's name is never accepted. * @return bool */ public static function matches($expected, $typed, array $allNames = []) { $typedForm = self::normalize($typed); if ($typedForm === '' || strlen($typedForm) > 60) { return false; } $accepted = self::forms($expected); if (in_array($typedForm, $accepted, true)) { return true; } // The other countries (everything that isn't the same country as $expected). $others = []; foreach ($allNames as $other) { $otherForms = self::forms($other); if (array_intersect($accepted, $otherForms)) { continue; // the same country } $others[] = $otherForms; } foreach ($others as $otherForms) { if (in_array($typedForm, $otherForms, true)) { return false; // the name of a different country } } // One slipped letter in a long name is forgiven... $slip = false; foreach ($accepted as $form) { if (strlen($form) >= 8 && levenshtein($typedForm, $form) <= 1) { $slip = true; break; } } if (!$slip) { return false; } // ...unless it is just as close to a different country ("Sorth Korea"). foreach ($others as $otherForms) { foreach ($otherForms as $form) { if (strlen($form) >= 8 && levenshtein($typedForm, $form) <= 1) { return false; } } } return true; } }