DarkRiDDeR7 мин

Пишем аналог функции PHP preg_match_all на языке программирования D

DLangРегулярные выражения

В PHP есть очень удобная функция для глобального поиска шаблона регулярного выражения в строке preg_match_all. Давайте напишем аналогичный класс статических методов для реализации этой функции с разными флагами на языке программирования D.

Описание функции:

``` int preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] )


Функция ищет в строке *subject* все совпадения с шаблоном pattern и помещает результат в массив *matches* в порядке, определяемом комбинацией флагов *flags*.
 После нахождения первого соответствия последующие поиски будут осуществляться не с начала строки, а от конца последнего найденного вхождения.

Данная функцию очень удобна и чаще всего она применяется с третьим параметром, чтобы обработать полученный массив совпадений шаблона. В *D* подобной регулярной функции нет. Мне вообще не сильно нравятся, как реализованы регулярные выражения в *D*. Ну да ладно.

Полную документацию по функции можете посмотреть здесь [http://php.net/manual/ru/function.preg-match-all.php](http://php.net/manual/ru/function.preg-match-all.php).

Возможные флаги функции *preg_match_all*:

- *PREG_PATTERN_ORDER* – упорядочивает результаты так, что элемент *$matches[0]* содержит массив полных вхождений шаблона, элемент *$matches[1]* содержит массив вхождений первой подмаски, и так далее;
- *PREG_SET_ORDER* – упорядочивает результаты так, что элемент *$matches[0]* содержит первый набор вхождений, элемент *$matches[1]* содержит второй набор вхождений, и так далее;
- *PREG_OFFSET_CAPTURE* – в случае, если этот флаг указан, для каждой найденной подстроки будет указана ее позиция в исходной строке. Необходимо помнить, что этот флаг меняет формат возвращаемого массива matches в массив, каждый элемент которого содержит массив, содержащий в индексе с номером *0* найденную подстроку, а смещение этой подстроки в параметре *subject* — в индексе *1*.

Не будем заморачиваться с реализацией через передачу константы, как в *PHP,* или через шаблоны в *D*. Создадим для каждого флага *PHP*данной функции аналогичные статические методы. Параметр *offset* оставим за бортом. Класс давайте назовём *PReg*. Вырисовывается следующая структура:

class PReg { public static:

… matchAllPatternOrder (...) {...} … matchAllSetOrder (...){...} … matchAllOffsetCapture (…) {...} }


Как параметры будем передавать строку для поиска (тип *string*), регулярное выражение (тип *???*) и переменную для вывода количества совпадений шаблона регулярного выражения (тип *out int*). Тип регулярного выражения можно посмотреть, чтобы не капаться в модуле, применив такую хитрость:

writeln(typeof(regex(\d+)).stringof);


Также как аргумент вы без проблем сможете передавать compile-time регулярное выражение (*ctRegex*). Для данного типа создадим алиас типа, чтобы поудобнее было.
 Функция *matchAllPatternOrde*r и *matchAllSetOrde*r будет возвращать двумерный массив строк, для них тоже создадим алиас типов (мне так кажется, что путаницы меньше). *MatchAllOffsetCapture* пока оставим на закуску.

В итоге у нас получается:

class PReg { private static alias typeRegex = Regex!char;

public static: alias typePatternOrder = string[]; alias typeSetOrder = string[];

typePatternOrder[] matchAllPatternOrder (string subject, typeRegex obRegex, out int count) {..} typeSetOrder[] matchAllSetOrder (string subject, typeRegex obRegex, out int count){...} … matchAllOffsetCapture (string subject, typeRegex obRegex, out int count) {...} }


Привожу пример реализованной функции *matchAllPatternOrder* (основные моменты пояснены ниже):

typePatternOrder[] matchAllPatternOrder (string subject, typeRegex obRegex, out int count) { typePatternOrder[] matches; auto stdMatches = matchAll(subject, obRegex);

while (!stdMatches.empty) { matches.length++; matches[0] ~= stdMatches.front.hit;

for (int i = 1; i < stdMatches.front.length; i++) { matches.length++; matches[count+1] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }

- Функция *matchAll* осуществляет глобальный поиск шаблона регулярного выражения obRegex.
- Цикл *while* файл перебирает найденные совпадения пока они не закончатся (проверка с помомощью *stdMatches.empty*);
- *stdMatches.front* хранит строку и найденные подстроки;
- *stdMatches.front.hit* возвращает всё строку входящую в найденный шаблон;
- *stdMatches.front[i]* возвращает найденные части по позиции маски;
- *stdMatches.popFront(*) переход к следующей найденной строке.

Реализация функции *matchAllSetOrder* по сути ничем сильно не отличается кроме как позициями найденных строк в массиве (она даже проще). Приведу лишь реализованный вариант:

typeSetOrder[] matchAllSetOrder (string subject, typeRegex obRegex, out int count) { typeSetOrder[] matches; auto stdMatches = matchAll(subject, obRegex);

while (!stdMatches.empty) { for (int i = 0; i < stdMatches.front.length; i++) { matches.length++; matches[count] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }


Реализация функции *matchAllOffsetCapture* имеет свои особенности. Найденные данные функции будем хранить в двумерном массиве картежа типов *Tuple!(string, «text», int, «position»)*, по которому можно будет получить позицию и найденую строку. Реализация:

alias typeOffsetCapture = Tuple!(string, "text", int, "position")[];

typeOffsetCapture[] matchAllOffsetCapture (string subject, typeRegex obRegex, out int count) { typeOffsetCapture[] matches;

auto stdMatches = matchAll(subject, obRegex); matches.length = stdMatches.front.length;

while (!stdMatches.empty) { Tuple!(string, "text", int, "position") match; match.text = stdMatches.front.hit; match.position = cast(int)(match.text.ptr - subject.ptr); matches[0].length = count+1; matches[0][count] = match; for (int i = 1; i < stdMatches.front.length; i++) { matches[i].length = matches[0].length; match.text = stdMatches.front[i]; match.position = cast(int)(stdMatches.front[i].ptr - subject.ptr); matches[i][count] = match; } stdMatches.popFront(); count++; } return matches; }


Единственное, что здесь важно отметить, как определяются позиции, а именно:

match.position = cast(int)(match.text.ptr - subject.ptr);


Позиция определяется через разность указателей позиций элементов массива. Также здесь выполняется приведение типа к *int*, так как многие процессоры уже использует 64-х битное представление, и указатели будут иметь тип*long*.

Полная реализация модуля с тестами представлена ниже:

module preg;

import std.string: format; import std.typecons; import std.regex: Regex, regex, ctRegex, matchFirst, matchAll;

// analog of preg regex in php

class PReg { private static alias typeRegex = Regex!char;

public static:

alias typePatternOrder = string[]; alias typeSetOrder = string[]; alias typeOffsetCapture = Tuple!(string, "text", int, "position")[];

typePatternOrder[] matchAllPatternOrder (string subject, typeRegex obRegex, out int count) { typePatternOrder[] matches; auto stdMatches = matchAll(subject, obRegex);

while (!stdMatches.empty) { matches.length++; matches[0] ~= stdMatches.front.hit;

for (int i = 1; i < stdMatches.front.length; i++) { matches.length++; matches[count+1] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }

typeSetOrder[] matchAllSetOrder (string subject, typeRegex obRegex, out int count) { typeSetOrder[] matches;

auto stdMatches = matchAll(subject, obRegex);

while (!stdMatches.empty) { for (int i = 0; i < stdMatches.front.length; i++) { matches.length++; matches[count] ~= stdMatches.front[i]; } stdMatches.popFront(); count++; } return matches; }

typeOffsetCapture[] matchAllOffsetCapture (string subject, typeRegex obRegex, out int count) { typeOffsetCapture[] matches;

auto stdMatches = matchAll(subject, obRegex); matches.length = stdMatches.front.length;

while (!stdMatches.empty) { Tuple!(string, "text", int, "position") match; match.text = stdMatches.front.hit; match.position = cast(int)(match.text.ptr - subject.ptr); matches[0].length = count+1; matches[0][count] = match; for (int i = 1; i < stdMatches.front.length; i++) { matches[i].length = matches[0].length; match.text = stdMatches.front[i]; match.position = cast(int)(stdMatches.front[i].ptr - subject.ptr); matches[i][count] = match; } stdMatches.popFront(); count++; } return matches; } }

unittest { import std.stdio; writeln("test PReg.matchAll");

int count;

auto matches1 = PReg.matchAllPatternOrder(one two, regex((\w)(\w)\w+), count); assert(matches1[0][0] == one); assert(matches1[0][1] == two); assert(matches1[1][0] == o); assert(matches1[1][1] == n); assert(matches1[2][0] == t); assert(matches1[2][1] == w); assert(count == 2);

auto matches2 = PReg.matchAllSetOrder(one two, regex((\w)(\w)\w+), count); assert(matches2[0][0] == one); assert(matches2[0][1] == o); assert(matches2[0][2] == n); assert(matches2[1][0] == two); assert(matches2[1][1] == t); assert(matches2[1][2] == w); assert(count == 2);

auto matches3 = PReg.matchAllOffsetCapture(one two, ctRegex!((\w)(\w)\w+), count); assert(matches3[0][0].text == "one"); assert(matches3[0][0].position == 0); assert(matches3[0][1].text == "two"); assert(matches3[0][1].position == 4);

assert(matches3[1][0].text == "o"); assert(matches3[1][0].position == 0); assert(matches3[1][1].text == "t"); assert(matches3[1][1].position == 4);

assert(matches3[2][0].text == "n"); assert(matches3[2][0].position == 1); assert(matches3[2][1].text == "w"); assert(matches3[2][1].position == 5); assert(count == 2); }


Надеюсь, что из этой статьи вы подчеркнули для себя что-то полезное и интересное. Всем спасибо!

## Как довести класс до рабочего состояния

После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.

assert(matchAll(r"(w+)=(d+)", "a=1 b=22").length == 2); assert(matchAll(r"z+", "abc").empty);


В D стоит явно решить, какой формат результата нужен. PHP поддерживает несколько режимов: *PREG_PATTERN_ORDER*, *PREG_SET_ORDER* и вариант с *PREG_OFFSET_CAPTURE*. Если пытаться сделать всё сразу, код быстро станет запутанным. Лучше сначала реализовать простой режим, затем режим группировки по совпадениям, и только после этого добавить позиции.

struct MatchPart { string text; ptrdiff_t offset; }

alias MatchSet = MatchPart[][];


Отдельно проверьте Unicode. Если шаблон и строка содержат кириллицу, важно понимать, что именно возвращается в offset: позиция в байтах или позиция в символах. PHP для *PREG_OFFSET_CAPTURE* возвращает байтовое смещение. Если в D вы хотите повторить поведение PHP, нужно документировать именно байтовый offset, иначе результаты будут отличаться.

Финальный класс должен иметь маленькую поверхность API: метод без offset, метод с offset и понятный enum для порядка результата. Тогда аналог *preg_match_all* будет не просто копией PHP-функции, а удобным D-инструментом, который предсказуемо работает в тестах.