В 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-обработчика отдельно сравните его реальные паттерны и флаги: одинаковая форма массива ещё не гарантирует одинаковый язык регулярных выражений.