Wyrażenia regularne – część 1 (dopasowanie liczb)

Jak wspominałem w pierwszym wpisie dotyczącym wyrażeń regularnych nie będę zamieszczał gotowych zestawień całej składni lecz będziemy kolejno wprowadzać nowe elementy na konkretnych przykładach. Dzisiaj przyjrzymy się problematyce wyszukiwania numeru telefonu w ciągu tekstowym.

Naszym pierwszym zadaniem będzie dopasowanie wzorca, który wyszuka w zadanym tekście (obiekt typu string) numer telefonu i przypisze go do zmiennej „tel”. Przeanalizujemy krok po kroku poniższy kod.

var str = "Jeśli masz jakieś pytania to dzwoń: 607-784-441",
    tel = str.match(/[0-9]{3}-[0-9]{3}-[0-9]{3}/)[0];

tel; //"607-784-441"

Na początku zadeklarowaliśmy zmienną „str” przechowującą analizowany ciąg tekstowy, w którym znajduje się szukany numer telefonu zapisany w formacie xxx-xxx-xxx.

Metoda match pobiera jeden argument – wzorzec wyrażenia regularnego i zwraca tablicę dopasowań lub null w przypadku braku możliwości dopasowania wyrażenia regularnego.

W naszym przykładzie z góry wiemy, że istnieje dokładnie jeden numer telefonu dlatego zmiennej tel przypisujemy znalezione dopasowanie, czyli pierwszy element tablicy zwracanej przez metodę match (element z indeksem zerowym). W dalszej części wpisu zobaczymy jakie problemy wiążą się z takimi założeniami i jak sobie z nimi poradzić.

Przeanalizujmy zatem dokładnie nasz wzorzec wyrażenia regularnego:

  • stworzyliśmy obiekt RegExp w zapisie literalnym z użyciem „/…/”
  • nawiasy kwadratowe oznaczają zakresy, czyli w naszym przypadku zapis [0-9] oznacza wzorzec, który dopasuje dowolną cyfrę.
  • Założyliśmy jednak, że numer podany jest w formacie xxx-xxx-xxx, więc musimy wyszukać trzy cyfry znajdujące się obok siebie. W tym celu obok zapisu [0-9] dodajemy tzw. kwantyfikator określający ilość wystąpień danego wzorca. W naszym wypadku zapis „{3}” oznacza dokładnie trzy wystąpienia.
  • Kolejnym szukanym elementem jest myślnik.
  • Za myślnikiem powinny znaleźć się kolejne trzy cyfry, znowu myślnik i ostatnia grupa trzech cyfr.

W ten oto sposób znaleźliśmy dopasowanie naszego numeru telefonu.

Czy na pewno wzorzec działa prawidłowo?

Na pierwszy rzut oka może wydawać się, że kod działa prawidłowo. Co jednak, gdy stwierdzimy, że przecież w tekście może wystąpić kilka numerów telefonów, więc może jednak lepiej zapisać je w tablicy, a nie w zmiennej typu string. Usuńmy zatem odniesienie do zerowego elementu tablicy zwracanej przez metodę match.

var str = "Mój numer to 607-784-441 lub 444-555-666"
    tel = str.match(/[0-9]{3}-[0-9]{3}-[0-9]{3}/);

tel; //["607-784-441"] ups, to tylko jeden numer!

tel = str.match(/[0-9]{3}-[0-9]{3}-[0-9]{3}/g);
tel; //["607-784-441", "444-555-666"] teraz jest ok

Zwróć uwagę na drobny szczegół różniący oba wzorce dopasowania – literę „g” za ostatnim (zamykającym) znacznikiem „/”. Jest to tzw. flaga globalna, mówiąca metodzie match, że ma przeszukiwać cały tekst. Bez flagi „g” metoda match będzie szukać dopasowania wyłącznie do pierwszego powodzenia. Dlatego pierwsze wywołanie „tel” zwróciło jeden numer.

JavaScript obsługuje kilka flag dla wyrażeń regularnych, z czego najczęściej używane to „g” (wyszukiwanie globalne w całym ciągu) oraz „i” (wyszukiwanie wzorca bez względu na wielkość liter). Użycie flagi „i” zostanie omówione w oddzielnym wpisie, gdyż nie zawsze działa ona tak, jakbyśmy tego oczekiwali, szczególnie gdy przeszukujemy ciągi zapisane z użyciem liter spoza alfabetu zawartego w ASCII oraz ciągów zapisywanych w formacie znaków Unicode.

Nieco więcej informacji o kwantyfikatorach

No dobrze, mamy zatem wzorzec, który potrafi znaleźć wiele numerów telefonów w naszym tekście. Ale co się stanie, gdy z jakiś względów część numerów będzie zapisanych w formacie xxx-xxx-xxx, a część bez użycia myślników (xxxxxxxxx)? Nasz wzorzec w obecnej postaci niestety znajdzie tylko numery zawierające myślniki. Spróbujmy naprawić ten problem:

var str = "Mój numer to 607-784-441 lub 444555666"
    tel = str.match(/[0-9]{3}-?[0-9]{3}-?[0-9]{3}/g);

tel; //["607-784-441", "444555666"]

Zastosowaliśmy tutaj kolejny kwantyfikator oznaczany symbolem znaku zapytania „?”, który oznacza, że element znajdujący się przed nim może wystąpić zero lub jeden raz.

Zastosowaliśmy kwantyfikator „?” po każdym myślniku, więc pozwalamy na dopasowanie numeru telefonu wg obu wcześniej wymienionych wzorców. No dobrze, ale idąc dalej tym tropem możemy dojść do wniosku, że przecież numery telefonów zapisujemy też często w formacie ” xxx xxx xxx” stosując spację w miejscu myślników. Czeka nas zatem kolejna modyfikacja wzorca.

var str = "Mój numer to 607-784-441, 444 555 666 i 123321123"
    tel = str.match(/[0-9]{3}[ -]?[0-9]{3}[ -]?[0-9]{3}/g);

tel; //["607-784-441", "444 555 666", "123321123"]

Wcześniej pisałem, że kwantyfikator „?” odnosi się do elementu, który się znajduje przed znakiem zapytania. Otóż elementem tym może być pojedynczy znak, zakres znaków wyrażony nawiasami „[ … ]” lub grupa oznaczana jako „( … )”.

W naszym przypadku mamy do czynienia z przypisaniem kwantyfikatora do grupy znaków „[ -]”, co oznacza, że dowolny ze znaków zawartych między nawiasami kwadratowymi wystąpi zero lub jeden raz (spację można zapisać jako ” ” lub „\x20” – zapis Unicode).

Wiemy już więc co oznacza kwantyfikator „?”. W JavaScript jest kilka różnych kwantyfikatorów, wśród których warto wymienić w tym momencie również znak plusa „+” oznaczający co najmniej jedno wystąpienie oraz znak gwiazdki „*” oznaczający dowolną ilość wystąpień danego elementu.

Uporządkujmy zatem poznane kwantyfikatory

Ok, poznaliśmy do tej pory cztery rodzaje kwantyfikatorów (?, +, *, {3}). W naszym wzorcu RegExp za każdym razem stosowaliśmy kwantyfikator {3}, który oznacza „dokładnie trzy wystąpienia” elementu poprzedzającego (u nas dotyczy to grupy [0-9] oznaczającej dowolną cyfrę).

Kwantyfikator wykorzystujący nawiasy klamrowe może przyjmować jeden lub dwa parametry. W przypadku podania tylko jednej liczby oznacza on dokładną liczbę wystąpień. Często stosuje się również formę dwuparametrową, gdzie pierwsza liczba oznacza minimalną ilość wystąpienia elementu, a druga liczba ilość maksymalną.

Zanim przejdziemy do przykładów omówimy jeszcze jeden element składni wyrażeń regularnych jakim są granice. Istnieje kilka rodzajów granic, ale najczęściej używane to początek i koniec ciągu znakowego. Oznacza je się jako „^” oraz „$”. Znaczenie symbolu „^” zmienia się jeśli użyjemy go wewnątrz nawiasów kwadratowych ale o tym za chwilę.

Załóżmy, że mamy dany ciąg cyfr (zapisany w formie tekstowej jako obiekt string) i chcemy zweryfikować, czy na pewno w ciągu tym są wyłącznie same cyfry:

/^[0-9]+$/.test("12345"); //true
/^[0-9]+$/.test("aaa12345"); //false
/^[0-9]+$/.test("12345bbb"); //false
/^[0-9]+$/.test("aaa12345bbb"); //false

Użyliśmy tutaj dla odmiany metody test, którą należy wywoływać na obiekcie RegExp i jako argument przekazać ciąg znakowy do analizy. Na początku wzorca zastosowaliśmy symbol „^”, a po nim grupę „[0-9]” co oznacza, że na początku analizowanego ciągu może wystąpić wyłącznie dowolna cyfra. Dalej znajduje się kwantyfikator „+” oznaczający, że cyfra ta musi wystąpić nie mniej niż jeden raz, a symbol „$” mówi o tym, że poprzedzający go element (czyli nasza ostatnia cyfra) musi być ostatnim elementem ciągu znakowego.

W bardziej złożonych wzorcach symbole „^” oraz „$” wcale nie muszą znajdować się na początku i końcu wzorca wyrażenia regularnego, ale do tego zagadnienia powrócimy w kolejnych wpisach.

Jak widać metoda test zwróciła true tylko dla pierwszego przypadku, gdy faktycznie przekazany ciąg zawierał same cyfry. W każdej innej sytuacji zwrócona została wartość false.

/^[0-9]{3}$/.test("607"); //true
/^[0-9]{3}$/.test("4456"); //false

/^[0-9]{3,4}$/.test("607"); //true
/^[0-9]{3,4}$/.test("4456"); //true

/^[0-9]{3,}$/.test("607"); //true
/^[0-9]{3,}$/.test("4456"); //true
/^[0-9]{3,}$/.test("78"); //false

/^[0-9]{0,3}$/.test("78"); //true
/^[0-9]{0,3}$/.test("607"); //true
/^[0-9]{0,3}$/.test("4456"); //false

No dobrze, omówmy więc krótko nasz przykład. Na początku testujemy wystąpienie dokładnie trzech cyfr, co jest spełnione tylko dla „607”. Kolejne dwa zapisy sprawdzają, czy ciąg zawiera minimum 3, ale nie więcej nić 4 cyfry – określa to zapis: „{min, max}”.

Jeśli w zapisie tym pominiemy wartość „max” (ale pozostawimy przecinek!) to kwantyfikator będzie oznaczał co najmniej „min” (u nas co najmniej trzy) wystąpienia dowolnej cyfry, bez określania górnej granicy. W naszym przykładzie ciąg musi zawierać co najmniej trzy cyfry.

Co jednak gdy chcemy określić wzorzec określający wystąpienie maksymalnie „max” dopasowań bez podawania dolnej granicy? Otóż w tym wypadku nie należy pomijać pierwszego członu kwantyfikatora lecz po prostu jako pierwszy parametr („min”) podać wartości zerową (nie może przecież wystąpić ujemna ilość dopasowań).

Omówione wcześniej kwantyfikatory „?”, „+” oraz „*” można traktować jako krótszą wersję zapisu wykorzystującego kwantyfikator „{min, max}” i tak:

  • kwantyfikator „?” jest tożsamy z zapisem „{0,1}”,
  • kwantyfikator „+” jest tożsamy z zapisem „{1,}”,
  • kwantyfikator „*” jest tożsamy z zapisem „{0,}”.

Istnieje jeszcze pewien problem tzw. zachłanności kwantyfikatorów, z którym być może spotkałeś się w niektórych poradnikach i książkach, ale zagadnienie to omówimy w oddzielnym wpisie z analizą szczegółowych przykładów praktycznych.

Powróćmy na chwilę do zapisu zakresów znaków „[ … ]”

W naszych przykładach co chwilę powtarzaliśmy zapis „[0-9]” oznaczający dowolną cyfrę z podanego zakresu, czyli od zera do dziewięciu. Można jednak podać praktycznie dowolny zakres cyfr, np.:

/^[0-9]+$/.test("123456789"); //true
/^[2-5]+$/.test("123456789"); //false
/^[2-5]+$/.test("2245"); //true

Należy jednak pamiętać, że znak myślnika oznaczający zakres (podany w nawiasach kwadratowych) odnosi się wyłącznie do dwóch znaków: znajdującego się przed i za myślnikiem. Spójrzmy na poniższy przykład:

/^[2-40]+$/.test("2345"); //false ups...
/^[2-40]+$/.test("234"); //true
/^[2-40]+$/.test("0234"); //true

W pierwszym wywołaniu metody test planowaliśmy sprawdzić, czy ciąg zawiera liczby w zakresie od 2 do 40, jednak wbrew temu czego oczekiwaliśmy wynikiem jest false.

Zobaczmy co się dzieje w kolejnych wywołaniach metody test. Otóż składnia wyrażeń regularnych i zapisów zakresowych (z nawiasami kwadratowymi) interpretuje nasz zapis [2-40] jako zakres od 2 do 4 lub cyfra 0. Sytuacja wyjaśni się bardziej gdy będziemy omawiać grupy znaków i tzw. alternatywy (oznaczane symbolem „|”). Istnieją pewne sztuczki pozwalające uzyskać wyrażenie regularne, które faktycznie będzie testować zakres od 2 do 40 ale jest to temat na oddzielny artykuł (napiszę o tym np. przy okazji testowania poprawności adresów IP).

Z zakresami związany jest jeszcze jeden niuans dotyczący zmiany zachowania znaku „^”. Znak ten użyty po za nawiasami kwadratowymi odnosi się do początku analizowanego ciągu (lub początku linii jeśli użyjemy flagi „m”, ale o tym kiedy indziej). Jeśli jednak symbol „^” użyjemy wewnątrz nawiasów kwadratowych to będzie on oznaczał negację podanego zakresu, czyli skoro zapis [0-9] oznacza dowolną cyfrę, to zapis [^0-9] oznacza wszystko poza dowolną cyfrą:

/^[0-9]+$/.test('123'); //true
/^[^0-9]+$/.test('123'); //false

/^[0-9]+$/.test('abc'); //false
/^[^0-9]+$/.test('abc'); //true

Poznaliśmy dość sporo elementów składni wyrażeń regularnych i jako zadanie domowe pozostawiam czytelnikom rozważenie kwestii dlaczego w ostatnich przykładach zrezygnowałem z dodatkowej flagi „g” oznaczającej przeszukanie całego ciągu? Jeśli uważnie czytałeś część związaną z granicami dopasowania to bez problemu powinieneś odpowiedzieć na to pytanie.

Tak na zakończenie dodam tylko, że często stosowany przez nas zapis „[0-9]” można zastąpić skróconą wersją „\d”.