Pull to refresh

Умляуты в регулярных выражениях

Reading time6 min
Views10K
Умла́ут или реже умля́ут (нем. Umlaut) — диакритический знак, указывающий на фонетическое явление умлаута гласных звуков в немецком и некоторых других языках. Обычно изображается в виде двух точек над буквой, в готическом же шрифте традиционно (за исключением последних десятилетий) выглядел как маленькая надстрочная буква e. В немецком присутствует в таких буквах, как Ää, Öö и Üü (называемых «A-умляут», «O-умляут» и «U-умляут» соответственно).
Wikipedia


Предисловие


Пришлось мне делать очередную валидацию формочки регистрации. Мысль о том, что могут возникнуть трудности мне в голову не приходила. Но это была формочка регистрации на международной конференции и начальство после проверки валидации намекнуло, что для проверки имени или фамилии простое /^[A-Za-z\s\.-]+$/ не прокатит. Ведь гостями конференции могут быть и ребята из таких замечательных стран, как Испания, Франция Германия и др., в языках которых как раз и присутствуют эти пресловутые «умляуты» и другие спецсимволы над буквами.

Поиск решения


Когда я задумался над этим вопросом в голову ничего не пришло кроме перечисления или всех «спецбукв», которые могут быть в ФИО участника конференции или наоборот, всего что не должно содержаться в введенных данных. Но что один, что другой диапазон символов просто неимоверно велик. Перечислять все глупо и долго, а если перечислять только часть — теряется точность регулярки.
Я пошел гуглить и решения так и не нашел, а добавить поддержку «умляутов» сказали буквально перед сдачей этой доработки. Время поджимало, и решил что дальше искать готовую регулярку или решение проблемы безрезультатно (даже на хабре решения не нашел, хотя тема один раз подымалась). Решил прикрутить «моторчик к велосипеду» сам.

Решение


Я решил оттолкнуться от чего-то, обобщающего эти символы. Начал тестить их кодирование разными алгоритмами, сравнивать коды символов в unicode таблице, но мысли по решению начали появляться только после конвертации «спецбукв» в мнемоники (или как их еще называют HTML-сущности или entities).
В виде сущности "ä" будет выглядеть как "& auml;" (пробел от хабрапарсера) и остальные в том же духе.
Для таких преобразований в javascript я нашел готовое решение (источник javascript.ru/node/10853):

var HTML=function(){
   var x,mnem=
   {34:"quot",38:"amp",39:"apos",60:"lt",62:"gt",402:"fnof",
   338:"OElig",339:"oelig",352:"Scaron",353:"scaron",
   376:"Yuml",710:"circ",732:"tilde",8226:"bull",8230:"hellip",
   8242:"prime",8243:"Prime",8254:"oline",8260:"frasl",8472:"weierp",
   8465:"image",8476:"real",8482:"trade",8501:"alefsym",8592:"larr",
   8593:"uarr",8594:"rarr",8595:"darr",8596:"harr",8629:"crarr",
   8656:"lArr",8657:"uArr",8658:"rArr",8659:"dArr",8660:"hArr",
   8704:"forall",8706:"part",8707:"exist",8709:"empty",8711:"nabla",
   8712:"isin",8713:"notin",8715:"ni",8719:"prod",8721:"sum",
   8722:"minus",8727:"lowast",8730:"radic",8733:"prop",8734:"infin",
   8736:"ang",8743:"and",8744:"or",8745:"cap",8746:"cup",8747:"int",
   8756:"there4",8764:"sim",8773:"cong",8776:"asymp",8800:"ne",
   8801:"equiv",8804:"le",8805:"ge",8834:"sub",8835:"sup",8836:"nsub",
   8838:"sube",8839:"supe",8853:"oplus",8855:"otimes",8869:"perp",
   8901:"sdot",8968:"lceil",8969:"rceil",8970:"lfloor",8971:"rfloor",
   9001:"lang",9002:"rang",9674:"loz",9824:"spades",9827:"clubs",
   9829:"hearts",9830:"diams",8194:"ensp",8195:"emsp",8201:"thinsp",
   8204:"zwnj",8205:"zwj",8206:"lrm",8207:"rlm",8211:"ndash",
   8212:"mdash",8216:"lsquo",8217:"rsquo",8218:"sbquo",8220:"ldquo",
   8221:"rdquo",8222:"bdquo",8224:"dagger",8225:"Dagger",8240:"permil",
   8249:"lsaquo",8250:"rsaquo",8364:"euro",977:"thetasym",978:"upsih",982:"piv"},
   tab=("nbsp|iexcl|cent|pound|curren|yen|brvbar|sect|uml|"+
   "copy|ordf|laquo|not|shy|reg|macr|deg|plusmn|sup2|sup3|"+
   "acute|micro|para|middot|cedil|sup1|ordm|raquo|frac14|"+
   "frac12|frac34|iquest|Agrave|Aacute|Acirc|Atilde|Auml|"+
   "Aring|AElig|Ccedil|Egrave|Eacute|Ecirc|Euml|Igrave|"+
   "Iacute|Icirc|Iuml|ETH|Ntilde|Ograve|Oacute|Ocirc|Otilde|"+
   "Ouml|times|Oslash|Ugrave|Uacute|Ucirc|Uuml|Yacute|THORN|"+
   "szlig|agrave|aacute|acirc|atilde|auml|aring|aelig|ccedil|"+
   "egrave|eacute|ecirc|euml|igrave|iacute|icirc|iuml|eth|ntilde|"+
   "ograve|oacute|ocirc|otilde|ouml|divide|oslash|ugrave|uacute|"+
   "ucirc|uuml|yacute|thorn|yuml").split("|");
   for(x=0;x<96;x++)mnem[160+x]=tab[x];
   tab=("Alpha|Beta|Gamma|Delta|Epsilon|Zeta|Eta|Theta|Iota|Kappa|"+
   "Lambda|Mu|Nu|Xi|Omicron|Pi|Rho").split("|");
   for(x=0;x<17;x++)mnem[913+x]=tab[x];
   tab=("Sigma|Tau|Upsilon|Phi|Chi|Psi|Omega").split("|");
   for(x=0;x<7;x++)mnem[931+x]=tab[x];
   tab=("alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|"+
   "lambda|mu|nu|xi|omicron|pi|rho|sigmaf|sigma|tau|upsilon|phi|chi|"+
   "psi|omega").split("|");
   for(x=0;x<25;x++)mnem[945+x]=tab[x];
   return {
     encode:function(text){
       return text.replace(/[\u00A0-\u2666<>\&]/g,function(a){
         return "&"+(mnem[a=a.charCodeAt(0)]||"#"+a)+";"
       })
     },
     decode:function(text){
       return text.replace(/\&#?(\w+);/g,function(a,b){
         if(Number(b))return String.fromCharCode(Number(b));
         for(x in mnem){
           if(mnem[x]===b)return String.fromCharCode(x);
         }
       })
     }
   }
}();

В ходе исследования конвертации различных символов я обнаружил, что данным скриптом символы стандартного набора конвертируются в самих себя («a»->«a», «z->»z", ";"->";" и т.д.), наши «спецбуквы» в мнемоники, спецсимволы таблицы юникода (всякие картинки и т.д.) — в вопросительные знаки.
Таким образом возникла идея такой проверки:
  1. После конвертации исходной фразы в мнемонику могут быть символы "&", ";" и "?" (если странный спецсимвол юникода или что-то другое неконвертируемое). Создаем паттерн для отсеивания этих символов из исходной фразы: /[;&?]+/
  2. Если фраза прошла проверку из п. 1 — конвертируем введенное слово или фразу в мнемонику
  3. Создаем паттерн проверки слова с мнемоникой: /^[A-Za-z\s\.-]+$/ плюс "&", ";", получаем /^[A-Za-z&;\s\.-]+$/i
  4. Если проверка в п. 3 пройдена — все ок!

Соответствующий код javascript (собственно мое решение):

function somefunc(word) // передаем в функцию исходную фразу
{
    hword = HTML.encode(word); // конвертируем фразу в мнемоники

    patt = /^[A-Za-z&;\s\.-]+$/; // regexp паттерн для проверки слова, конвертированного в мнемоники
    patt_ent = /[;&?]+/ig // regexp паттерн для проверки исходной фразы на символы будущей мнемоники

    if(patt_ent.test(word) === false)    // проверяем на наличие символов мнемоники в исходной фразе
    {
         return patt.test(hword); // false, если не прошло проверку основным регэкспом; true, если прошло
    }
    else return false; // фраза изначально содержит недопустимые символы (";", "&" или "?")
}

P.S.: Этот код не является конечным решением, а только решением проблемы. Т.е. заточить под свой валидатор вы уже можете сами и принимать-возвращать то что вам надо. Код растянул для читабельности моих комментариев к нему.
P.P.S.: С другими языками тоже проблем не должно быть. На PHP, думаю, это можно решить с помощью функции htmlentities() и подобных манипуляций (еще руки не дошли).

UPD: Спасибо хабраюзеру chainik за подсказку для PHP:
// adjust pattern to your needs
// $input needs to be UTF-8 encoded
if (preg_match('/^\p{L}+$/u', $input)) {
    // OK
} else {
    // not OK
}

\p{L} проверяет, выставлено ли у юникод-символа свойтво L («letter»).
Работает для всех языков безотносительно локали, главное кодировка utf-8

(с) by Stefan Gehrig from stackoverflow

Тестил я только в латинице, поэтому жду комментов по поводу тонких мест и того, чего я не учел, чтоб сформировать универсальное решение.
Tags:
Hubs:
+4
Comments20

Articles