| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- <?php
- /**
- * Decides whether what a student typed is the name of a country.
- *
- * Names in tk_countries come in the style of "Congo, Democratic Republic of
- * the" and "Vatican City (Holy See)", which nobody types. So every country
- * name is expanded into the forms a student might reasonably use:
- *
- * - capitals, accents, punctuation, HTML entities (e107 stores an apostrophe as
- * '), "&" / "and", "St" / "Saint" and a leading "the" are ignored
- * - "Congo, Republic of the" also accepts "Republic of the Congo"
- * - "Vatican City (Holy See)" also accepts "Vatican City" and "Holy See"
- * - well-known alternative names are accepted (USA, Holland, Ivory Coast,
- * Burma, Czech Republic ...): see $aliasGroups
- * - one slipped letter is forgiven in names of 8 or more letters
- * ("Kyrgystan"), but never when what was typed is exactly the name of a
- * different country ("Dominica" for the Dominican Republic), or is just as
- * close to a different country ("Sorth Korea")
- */
- class CountryNames {
- private static $translit = [
- 'à' => '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;
- }
- }
|