progcode_D / PHP / WEB

D

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

Обёртка над D std.regex.matchAll для группировки по подмаскам, по совпадениям и получения байтовых смещений с полным модулем и тестами.

В PHP preg_match_all ищет все совпадения и умеет группировать результат по подмаскам или по отдельным совпадениям. В D эту задачу можно построить поверх std.regex.matchAll. В заметке 2017 года предлагалась обёртка PReg; ниже сохранена её идея с исправленными индексами и обработкой пустого результата.

Какие режимы мы воспроизводим

  • PREG_PATTERN_ORDER: строка результата соответствует группе. Нулевой ряд содержит полные совпадения, первый — первые подмаски и так далее.
  • PREG_SET_ORDER: строка результата соответствует одному совпадению; внутри идут полное совпадение и его подмаски.
  • Захват смещения: вместе с текстом возвращается позиция в исходной UTF-8 строке. В этой обёртке результат группируется как в первом режиме.

Это адаптация формы результата, а не полная совместимость с PHP/PCRE. Синтаксис регулярных выражений, флаги, обработка отсутствующих групп и стартового offset должны обсуждаться отдельно. Здесь нет параметра начального смещения и общего битового набора флагов. Контракт PHP описан в справочнике preg_match_all.

Порядок групп на простом примере

subject = "one two"
pattern = (\w)(\w)\w+

По группам:                 По совпадениям:
["one", "two"]              ["one", "o", "n"]
["o",   "t"]                ["two", "t", "w"]
["n",   "w"]

Номер найденного слова и номер подмаски — разные координаты. Если записывать группу в result[count + 1], строки начнут зависеть от номера совпадения и схема станет неверной. Для режима по группам используется индекс подмаски i, а каждое новое слово дописывается в соответствующий ряд.

Диапазон результатов std.regex

Функция matchAll возвращает диапазон совпадений. У каждого элемента нулевая группа — всё совпадение; остальные элементы — подмаски. Цикл foreach не обращается к front пустого диапазона, поэтому строка без совпадений не требует опасного предварительного чтения. API описан в документации std.regex.

Параметр регулярного выражения сделан шаблонным: одна обёртка принимает результат regex и ctRegex, не навязывая обоим один конкретный тип. Счётчик имеет тип size_t, естественный для длины массива.

Полный модуль с проверками

module preg;

import std.regex : matchAll, regex, ctRegex;

struct Capture {
    string text;
    long position; // byte offset, -1 when an optional group did not participate
}

class PReg {
    static string[][] matchAllPatternOrder(R)(string subject, R re, out size_t count) {
        string[][] result;
        count = 0;
        foreach (hit; matchAll(subject, re)) {
            if (result.length == 0) result.length = hit.length;
            foreach (i; 0 .. hit.length) result[i] ~= hit[i];
            ++count;
        }
        return result;
    }

    static string[][] matchAllSetOrder(R)(string subject, R re, out size_t count) {
        string[][] result;
        count = 0;
        foreach (hit; matchAll(subject, re)) {
            string[] row;
            foreach (i; 0 .. hit.length) row ~= hit[i];
            result ~= row;
            ++count;
        }
        return result;
    }

    static Capture[][] matchAllOffsetCapture(R)(string subject, R re, out size_t count) {
        Capture[][] result;
        count = 0;
        foreach (hit; matchAll(subject, re)) {
            if (result.length == 0) result.length = hit.length;
            foreach (i; 0 .. hit.length) {
                auto part = hit[i];
                long position = part.ptr is null
                    ? -1
                    : cast(long)(part.ptr - subject.ptr);
                result[i] ~= Capture(part, position);
            }
            ++count;
        }
        return result;
    }
}

unittest {
    size_t count;
    auto pattern = regex(`(\w)(\w)\w+`);

    auto byGroup = PReg.matchAllPatternOrder("one two", pattern, count);
    assert(count == 2);
    assert(byGroup == [["one", "two"], ["o", "t"], ["n", "w"]]);

    auto byMatch = PReg.matchAllSetOrder("one two", pattern, count);
    assert(count == 2);
    assert(byMatch == [["one", "o", "n"], ["two", "t", "w"]]);

    auto offsets = PReg.matchAllOffsetCapture("one two", ctRegex!(`(\w)(\w)\w+`), count);
    assert(count == 2);
    assert(offsets[0][0] == Capture("one", 0));
    assert(offsets[0][1] == Capture("two", 4));
    assert(offsets[1][1] == Capture("t", 4));
    assert(offsets[2][1] == Capture("w", 5));

    assert(PReg.matchAllPatternOrder("---", pattern, count).length == 0);
    assert(count == 0);
    assert(PReg.matchAllSetOrder("", pattern, count).length == 0);
    assert(count == 0);
    assert(PReg.matchAllOffsetCapture("---", pattern, count).length == 0);
    assert(count == 0);

    auto moreGroups = PReg.matchAllPatternOrder("abc def", regex(`(\w)(\w)(\w)`), count);
    assert(moreGroups == [["abc", "def"], ["a", "d"], ["b", "e"], ["c", "f"]]);

    auto unicodeOffsets = PReg.matchAllOffsetCapture("я x", regex(`x`), count);
    assert(count == 1);
    assert(unicodeOffsets[0][0].position == 3); // UTF-8 bytes, not character index
}

Почему смещение — не номер символа

В D string хранит UTF-8 кодовые единицы. Разность указателей на срез и исходную строку даёт смещение в байтах. В строке "я x" буква x начинается с байта 3, хотя визуально перед ней два символа. Не выдавайте такое значение за индекс графемы интерфейса.

Поле позиции имеет тип long, чтобы не сужать обычное 64-разрядное смещение до 32-разрядного int. Неучаствовавшая необязательная группа обозначается позицией -1, если движок возвращает для неё срез с нулевым указателем. Если приложение должно различать все варианты пустого и отсутствующего захвата, зафиксируйте требуемую семантику отдельными тестами для выбранного движка.

Запуск и границы проверки

dmd -unittest -main -run preg.d

Проверки охватывают две формы результата, смещения, пустую строку, отсутствие совпадений, дополнительную подмаску и UTF-8. Для переноса существующего PHP-обработчика отдельно сравните его реальные паттерны и флаги: одинаковая форма массива ещё не гарантирует одинаковый язык регулярных выражений.

КОНЕЦ ЗАМЕТКИ↑ В начало