Wyrażenie regularne sprawdzające podane imię i nazwisko

Po kilkunastodniowej przerwie postanowiłem znowu powrócić na blogu do tematyki wyrażeń regularnych. Dziś omówimy wyrażenie regularne sprawdzające podane imię i nazwisko, co może znaleźć zastosowanie np. w formularzach kontaktowych czy systemach CRM. Jest to dobry przykład do omówienia podstawowych zasad składni RegExp w języku JavaScript.

Zastanawiałem się jaką formę wpisu wybrać aby przedstawić swój punkt widzenia na walidację imienia i nazwiska. Ostatecznie stwierdziłem, że stworzymy krok po kroku coraz bardziej rozbudowane wyrażenie tak, aby każdy mógł sam ocenić jak duża precyzja walidacji jest mu potrzebna.

Podstawowe założenia jakie przyjąłem to jedno pole do wpisania zarówno imienia jak i nazwiska. Generalnie w kodzie produkcyjnym, szczególnie przy tworzeniu RegExp powinniśmy stosować metodykę TDD (Test Driven Development). Chodzi o to, aby najpierw stworzyć solidny zestaw testów i dopiero później przystąpić do pisania kodu. Tutaj jednak zrobię nieco inaczej i kolejne kryteria, które powinny być uwzględnione w testach jednostkowych, będziemy wprowadzać na bieżąco. Pozwoli to na stopniowe omawianie składni RegExp i wdrażanie coraz bardziej rozbudowanego wyrażenia regularnego.

No to zaczynamy…

Nie będziemy się tutaj zajmować kodem HTML, gdyż wyrażenia regularne wcale muszą odnosić się do środowiska przeglądarki użytkownika. Równie dobrze można je stosować po stronie serwera jako druga lub dodatkowa, uzupełniająca walidacja dla metod wbudowanych (np. w framework). Zakładamy więc, że mamy pole, w którym użytkownik wpisuje imię i nazwisko, a którego wartość pobieramy jako String.

Najprostsze wyrażenie można sprowadzić do postaci:

 

let reg = /^[a-z]+\s[a-z]+$/i;

reg.test('Tomek Sochacki');   //true
reg.test('T S');              //true

reg.test('Tomek   Sochacki'); //false
reg.test('Tomek');            //false
reg.test('Anna Nowak-Kowal'); //false
reg.test('Mikołaj Nowak');    //false

Stworzyliśmy bardzo proste wyrażenie, w którym dopasowujemy dowolną literę a-z (co najmniej jeden raz), po której występuje tzw. biały znak (np. spacja) dokładnie jeden raz, i na końcu ponownie literę a-z co najmniej jeden raz. Do tego dokładamy flagę „i” (ignore case), dopasowującą zarówno małe jak i wielkie litery alfabetu.

Jak widać wzorzec ten poprawnie dopasował moje imię i nazwisko i same inicjały rozdzielone spacją. Wzorzec ma jednak pewne wady. Nie dopasowuje ciągu, w którym imię i nazwisko rozdziela kilka spacji, gdy podane zostanie tylko imię (lub tylko nazwisko), nie przepuszcza nazwisk wieloczłonowych (z myślnikiem w środku) oraz, co bardzo ważnie nie przepuszcza imion i nazwisk pisanych z polskimi znakami diaktrycznymi.

Celowo nie ująłem naszych narodowych znaków diaktrycznych gdyż często zauważam, że tego typu błąd popełnia wielu początkujących programistów. Dlatego tak ważne jest dobre przemyślenie testów i wszystkich możliwości… choć to w praktyce i tak jest nie możliwe (ludzie mają zbyt wiele pomysłów na to, jak źle wpisać teoretycznie proste informacje…).

Podejście numer dwa

Rozszerzymy więc nasze wyrażenie o polskie znaki diaktryczne oraz o możliwość rozdzielenia imienia i nazwiska kilkoma spacjami. Druga kwestia może budzić pewne wątpliwości, ale zdarzały mi się już przypadki, że użytkownicy wprowadzali 3-4 spacje „Bo tak ładniej wygląda jak jest rozsunięte”… Teoretycznie można by z tym walczyć, ale ja wychodzę z założenia, że czasami lepiej oszczędzić sobie trudnych (uwierz mi, na prawdę trudnych…) rozmów i przewidzieć niektóre błędy użytkownika odpowiednio je naprawiając. Na przykład tutaj możemy później użyć metody replace aby wszystkie wielokrotne białe znaki zamienić na pojedyncze spacje i dopiero taką wartość zapisywać do bazy.

let reg = /^[a-ząśżźćęółń]+\s*[a-ząśżźćęółń]+$/i;

reg.test('Tomek Sochacki');   //true
reg.test('T S');              //true

reg.test('Tomek   Sochacki'); //true -> poprawiono
reg.test('Tomek');            //false
reg.test('Anna Nowak-Kowal'); //false
reg.test('Mikołaj Nowak');    //true -> poprawiono

Pozostały nam dwa problemy – wpisanie wyłącznie imienia lub nazwiska oraz nazwiska dwuczłonowe z myślnikiem. Przyjrzyjmy się jednak na razie nieco bliżej drugiemu zagadnieniu.

Uwzględnienie nazwisk wieloczłonowych

Uważny czytelnik zwróci uwagę, że najpierw piszę o nazwiskach dwuczłonowych, a później (w tytule tego akapitu) o wieloczłonowych. Otóż należy sobie w tym momencie odpowiedzieć na pytanie ile członów może mieć nazwisko?

W większości przypadków odpowiedź to jeden lub dwa. W Polskim prawie nazwisko może być maksymalnie dwuczłonowe, jednakże w drodze odstępstwa kierownik USC może wyrazić zgodę na inne. Ponad to mówimy tu tylko o nazwiskach polskich, a co z zagranicznymi (np. nazwiska brytyjskie, szlacheckie itp.)?

Kolejna kwestia, zakładamy, że tylko nazwisko może być wieloczłonowe. A co z imieniem? W Polsce nie jest to praktykowane, ale np. we Francji zdarzają się również imiona wieloczłonowe…

Załóżmy więc, że dopuszczamy podanie imienia i nazwiska wg „typowych” zasad, czyli imię jednoczłonowe i nazwisko jedno lub dwuczłonowe (za chwilę powrócimy do wcześniej wspomnianych problemów narodowościowych):

let reg = /^[a-ząśżźćęółń]+\s*[a-ząśżźćęółń]+(?:\s*-\s*)?[a-ząśżźćęółń]+$/i;


reg.test('Anna Nowak-Kowal');   //true
reg.test('Anna Nowak - Kowal'); //true

No dobrze, mamy więc poprawne dopasowanie do nazwisk dwuczłonowych. Wyrażenie to można by zapisać nieco inaczej bez trzykrotnego powtarzania [a-ząśżźćęółń] z wykorzystaniem tzw. dopasowań wyprzedzających ale jest to blog dla osób początkujących więc nie chcę nadmiernie komplikować wzorca (taka forma wydaje mi się odpowiednio czytelna dla początkującego).

Pytanie czy prawidłowo zakładamy, że pomiędzy członami nazwiska i myślnikiem dopuszczone są spacje? Otóż tak… możesz wierzyć lub nie, ale użytkownicy wpisują często takie dane właśnie na oba sposoby.

Czy na pewno wzorzec jest poprawny…?

Teoretycznie wszystko na razie się zgadza i jedyne czego nie dopasowujemy pozytywnie to podanie samego imienia. Pytanie czy jest to dobre czy złe rozwiązanie?

Otóż zależy od sytuacji. Jeśli np. walidujemy dane na potrzeby prostego formularza kontaktowego to w zupełności wystarczy często podanie samego imienia. Moglibyśmy więc całe dopasowanie nazwiska ująć w grupę nieprzechwytującą i zastosować kwantyfikator zerowego lub jednokrotnego wystąpienia:

let reg = /^[a-ząśżźćęółń]+\s*(?:[a-ząśżźćęółń]+(?:\s*-\s*)?[a-ząśżźćęółń]+)?$/i;


reg.test('Anna'); //true

Całą grupę „nazwiska” dopasowujemy zero lub jeden raz, natomiast kwestię dwuczłonowości nazwiska pamiętaj, że rozwiązujemy już wewnątrz tej grupy.

A co jeśli wymagamy zarówno imienia jak i nazwiska?

Pytanie co zrobić, jeśli wymagamy podania zarówno imienia jak i nazwiska? Otóż w tym wypadku najlepszą metodą jest po prostu stworzenie dwóch oddzielnych pól i walidowanie ich prostymi regexp. W tym wypadku wzorzec będzie się głównie skupiał na dopuszczonych znakach a nie na wielokrotności występowania poszczególnych elementów. Jedyne o czym należy pamiętać, to zezwolenie na nazwiska wieloczłonowe – warto również zastanowić się nad imionami wieloczłonowymi.

Pozostają jeszcze dwa problemy… W każdym z powyższych wzorców zakładamy, że imię i nazwisko musi składać się z co najmniej jednej litery. Czy jest to poprawne? W praktyce nie, pytanie tylko jaką przyjąć dolną granicę?

Osobiście, jeśli potrzebuję stworzyć prostego regexp do walidacji imienia i nazwiska zakładam minimum trzy znaki dla każdego z nich, choć czasami warto rozważyć dwa znaki. Dlaczego? Otóż dlatego, że część osób zechce wpisać sobie przed imieniem tytuł, np. dr Jan Nowak itp. i uwierz mi, że na prawdę są przypadki, że ludzie tak chcą wpisywać dane.

Jeśli rozmawiamy o formularzu na stronie www typu wizytówka to nie ma to większego znaczenia, gdyż i tak w razie czego gdzieś będzie podany kontakt telefoniczny czy e-mail. Ale co jeśli projektujemy aplikację do zarządzania klientami / petentami i osoba wprowadzająca dane usilnie upiera się by właśnie tak je podawać…?

Powiesz – można jasno wskazać warunki wprowadzania danych w instrukcji… Ok, można… a co jeśli taki użytkownik będzie Ci wisiał na telefonie co drugi dzień bo „nie wchodzi mi imię…”?

Problem dopuszczonych znaków Unicode

Na koniec zostawiłem jeszcze jeden… de facto najważniejszy problem. Kto będzie używał naszej aplikacji?

Do tej pory w powyższych regexp zakładałem, że są to polacy lub osoby z nazwiskiem i imieniem składającym się wyłącznie z alfabetu łacińskiego + polskie znaki diaktryczne.

Czy założenie to jest poprawne? To zależy od przyjętego poziomu zaufania do walidowanych danych. Na przykład projektując prostą wizytówkę firmy, która z pewnością nie będzie wchodzić na rynki międzynarodowe można pozostać przy naszych założeniach z tego wpisu.

Jeśli jednak istnieje prawdopodobieństwo, że chcemy jako poprawne traktować imiona z innych alfabetów to musimy dopuścić większy zakres znaków Unicode.

Niestety w JavaScript nie mamy możliwości wskazania zakresów znaków Unicode dla poszczególnych alfabetów w wygodny sposób jak np. w Javie i musielibyśmy stosować dość rozbudowane zakresy znakowe (tak na prawdę to w regexp JavaScriptowych jeszcze wielu rzeczy brakuje…). Jednym z rozwiązań tego problemu jest dopuszczenie wszystkich znaków Unicode (symbol kropki „.”), z ewentualnym wykluczeniem „typowych” znaków specjalnych jak {}[]'";: itp. W praktyce jednak i tak pozostaje cała masa znaków Unicode, które nie są literami, a własnie różnego rodzaju znakami specjalnymi, a nawet śmiesznymi obrazkami.

W takiej sytuacji warto jednak zrobić krok wstecz i zastanowić się po co właściwie walidujemy imię i nazwisko? Często okazuje się bowiem, że interesuje nas jedynie czy pole zostało wypełnione, czyli np. wystarczy ograniczenie do sprawdzenia czy ciąg zawiera min. 2-3 znaki. I tyle.

A co jeśli ktoś wpisze w imieniu znaki specjalne? Nic. Prawdopodobieństwo takiej sytuacji jest bardzo małe i wystarczy odpowiednio zabezpieczyć się przed wprowadzeniem do bazy danych znaków niebezpiecznych. Można w tym celu np. zastosować kodowanie base64 lub inną metodę.

Podsumowanie

W moich wcześniejszych artykułach dotyczących wyrażeń regularnych w podsumowaniu najczęściej zamieszczałem ostateczną, doszlifowaną wersję wzorca regexp.

Tym razem tak nie zrobię. Dzisiejszy wpis nie miał na celu stworzenia jednego, uniwersalnego wzorca na walidację danych osobowych. Chciałem jedynie zwrócić uwagę na problematykę tworzenia regexp. Teoretycznie mogłoby się wydawać, że walidacja imienia i nazwiska to banalna sprawa… Jak się okazuje jest wręcz przeciwnie.

Jak zatem walidować imię i nazwisko pod kątem poprawności? Pytanie pierwsze – czy na pewno musimy sprawdzać poprawność wpisanych danych, czy może interesuje nas jedynie sam fakt uzupełnienia pola?

Jeśli mimo wszystko stwierdzisz, że chcesz walidować poprawność danych to cofnij się do początku tego artykułu i rozpisz dokładnie na kartce wszystkie założenia dla danych poprawnych i błędnych. Dopiero później zastanów się, czy na pewno dalej chcesz tworzyć do tego regexp…

Ponad to warto zawsze zastanowić się nad faktycznym poziomem zaufania do danych wejściowych. Wiele osób uważa, że nie można mieć zaufania do użytkownika.  W wielu sytuacjach owszem, ale czasami warto spojrzeć na problem nieco z boku. Na przykład czy projektując aplikację dla instytucji publicznej istnieje faktycznie duże ryzyko, że urzędnik będzie próbował wpisywać do aplikacji abstrakcyjne dane? Ponad to, czy projektujemy aplikację w pełni samodzielną, czy może aplikację, nad którą cały czas czuwa obsługa techniczna, która w razie problemu może „z ręki” wprowadzić dane, których nie przepuści walidacja?

Mimo faktu, że jestem wielkim fanem i pasjonatem tematyki wyrażeń regularnych to są przypadki, gdy świadomie odradzam ich stosowanie na rzecz innych, mniej skomplikowanych i szybszych metod. Na przykład nie uważam za sensowne odpalanie armaty w postaci regexp do sprawdzenia czy ciąg znakowy ma minimum 5 znaków, mimo, iż jest to przecież możliwe.

  • Nie ma sensu rozwijać UTF-16, skoro Sieć jest UTF-8 only i jest to zagwarantowane przez liczne fragmenty specyfikacji HTML czy DOM.

    Niemniej, jak sam zauważasz, flaga „u” pozwala używać przyjaźniejszej skladni regexów operujących na Unicode – a to duży plus. Owszem, nie ma takich fajnych grup jak w innych językach, niemniej nie dramatyzowałbym, że aż tak mocno to „ubrudzi” kod. Zwłaszcza, że mamy konstruktor RegExp i możemy mu przekazywać zapisane wcześniej w zmiennych nazwy takich grup 😉

    • z kosntruktorem się zgodzę, szczerze mówiąc to nawet nie wybrażam sobie, aby ktoś próbował takiego regexp napisać w składni „/ /” bez jakichkolwiek komentarzy, które w new RegExp można zrobić.
      Co do przyjaźniejszej składni o owszem, ale jak już pisałem tylko dla znaków ponad uFFFF. Dla zakresu BMP nie widzę większego sensu stosowania składni ES6, chyba, ale to już uważam za kwestię indywidualną. Tak na marginesie dodam tylko dla osób zaczynających przygodę z JS (co oczywiście nie dotyczy Ciebie:), że znaki ASCII również są zawarte w Unicode (jako pierwsze znaki), a dostęp do nich można uzyskać zarówno poprzez składnię z u, jak i x (jeśli podajemy dwa znaki w zapisie hex).

      A tak na marginesie, to jeśli ktoś musi testować regexp istnienie znaku z dowolnego alfabetu z poziomu JS to warto cofnąć się krok wstecz i sprawdzić, czy na pewno jest nam to niezbędne. Potężny wzorzec multi narodowy zawsze można stworzyć… pytanie tylko czy na pewno nakład pracy jest tego warty?

      Nie będę się jednak wgłębiał tutaj w standard Unicode, flagę „u” itp. gdyż de facto nie jest to główny sens mojego wpisu. Chętnie podejmę jednak taką dyskusję w jakimś z kolejnych wpisów, gdzie przyjrzę się właśnie tej tematyce.

      • https://developers.google.com/web/updates/2017/07/upcoming-regexp-features

        Czyżby regexy w JS w końcu miały przestać być ubogim krewnym tych z innych języków?

      • Fajnie by było, szczerze mówiąc zastanawia mnie dlaczego w JS tak słabo zaimplementowano regexp w porównaniu do innych języków. Najciekawsze są wg dopasowania wsteczne. Co do nazwanych grup to w zasadzie podchodzę obojętnie bo i tak uważam, że stosowanie zbyt wielu grup przechwytujących nie jest dobrym pomysłem i w razie takiej potrzeby warto cofnąć się krok wstecz i sprawdzić, czy na pewno dobrze zaprojektowaliśmy regexp i czy w ogóle regexp to w tej sytuacji najlepsze rozwiązanie.

        Ale pytanie czy ludzie faktycznie potrzebują regexp w codziennej pracy z JS, bo z tego co widzę to w necie bardzo mało się o tym pisze, a jeśli już to są to kopie tabelek ze składnią regexp (de facto często nie całą).

        I oczywiście trzeba było zapodać swoją składnią… „p{Alphabetic}” , nie można było przejąć z innych „p{L}” dla „dowolnej litery z dowolnego alfabetu Unicode” 🙂

        Dzięki za link, ciekawy artykuł, oby tylko faktycznie poszło to troszkę do przodu, choć na razie patrząc na to co powprowadzało ES6 to JS idzie w dobrą stronę…

  • > Niestety w JavaScript nie mamy możliwości wskazania zakresów znaków Unicode dla poszczególnych alfabetów

    Mamy! W ES6 weszły specjalne udogodnienia dla Unicode: https://mathiasbynens.be/notes/es6-unicode-regex