Замена по циклу в регулярном выражении

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

test1
test
test
test
test

test2
test
test

test3
test
test
test
test
test
test


Каждый блок начинается с теста с цифрой, а дальше случайное количество тестов. Надо в каждом обычном тесте заменить букву e на цифру теста, чтобы в итоге получилось:

test1
t1st
t1st
t1st
t1st

test2
t2st
t2st

test3
t3st
t3st
t3st
t3st
t3st
t3st

Я нахожу блоки

(test([0-9]+)\r\n)(((t)(e)(st\r\n))*)

Здесь \2 - это цифра теста, а \3 - это блок со случайным количеством тестов, в каждой строке которого надо сделать замену. Это вообще возможно?

\1(\5\2\7)*

То есть надо как-то заменить звёздочка раз.


Ответы (2 шт):

Автор решения: vsemozhebuty

Если ваш движок регулярных выражений поддерживает заглядывание назад с вариативной длиной (например, последние версии JavaScript), тогда можно попробовать так:

const str = `test1
test
test
test
test

test2
test
test

test3
test
test
test
test
test
test
`;

const result = str.replace(
  /(?<=test(\d)\r?\n(?:test\r?\n)*)test(?=\r?\n)/g,
  't$1st'
);

console.log(result);

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

Ваша регулярка включает в длину искомого фрагмента обе части: опорную, откуда берётся источник замены, и целевую, куда источник переносится.

Сперва «курсор» поиска находится в начале строки:

test1\ntest\ntest\ntest\ntest
^............................

Вот поиск находит подходящий фрагмент из двух частей:

test1\ntest\ntest\ntest\ntest
^...........|................

Совершается замена и курсор перемещается сразу после фрагмента:

test1\ntest\ntest\ntest\ntest
.............^...............

И всё — поиск уже не может увидеть первую опорную часть вашей регулярки и больше не сможет найти подходящий фрагмент, так как требуется, чтобы он начинался с этой опорной части.

Если мы используем для опорной части одну из проверок нулевой длины (zero length assertions), тогда мы движемся только по целевым частям. Ведь теперь регулярка не требует, чтобы искомый фрагмент начинался с опорной части, она ищет только целевую часть, а уже найдя её — заглядывает назад в поисках опорной части:

test1\ntest\ntest\ntest\ntest
.......^..|..................

test1\ntest\ntest\ntest\ntest
.............^..|............

test1\ntest\ntest\ntest\ntest
...................^..|......

test1\ntest\ntest\ntest\ntest
.........................^..|

UPD:

Судя под документации, Notepad++ использует PCRE на движке Boost. Это мощный движок, но заглядывание назад с вариативной длиной не поддерживается. Обычно в таких случаях при работе в редакторах приходится включать круговой поиск (галочка "Wrap Around" в Notepad++) и жать на кнопку замены, пока находятся соответствия по кругу. Однако в вашем случае нам везёт: мы можем свести регулярку всего к двум вариантам заглядывания назад и у нас целевая группа может стать опорной для следующей замены, поскольку цифра одна и та же и замены идут подряд. В общем, можно выкрутится так:

Найти: (?<=test(\d)\r\n)test(?=\r\n)|(?<=t(\d)st\r\n)test(?=\r\n)

Заменить всё на: t$1$2st

Пусть вас не смущает t$1$2st — в каждом случае или $1, или $2 будут пустой строкой и вставка её ничего не испортит.

Остаётся заметить, что эта регулярка будет работать, только пока искомое число состоит из одной цифры. Как только вместо \d нам нужно будет \d+, мы опять попадаем на неподдерживаемую вариативную длину в заглядывании назад.

Кстати, интересная особенность: Notepad++ заменяет этой регуляркой все фрагменты за один раз, после одного нажатия кнопки «Заменить всё». Пробовал в другом редакторе и пробовал глобальную замену в JavaScript — так не получается, заменяется только по одному фрагменту в группе, нужно много раз применять замену или жать на кнопку, пока всё не заменится. Выходит, Notepad++ при одном проходе поиска при заглядывании назад учитывает результаты уже сделанных замен, а при использовании других инструментов это может не учитываться, заглядывание назад видит только неизменённую версию строки.

→ Ссылка
Автор решения: Qwertiy

Я бы делал в 2 вложенные замены - так алгоритм получается линейным, а с предпросмотром назад - квадратичным.

var s = `test1
test
test
test
test

test2
test
test

test3
test
test
test
test
test
test`

console.log(s.replace(
  /^((test)(\d+)(\r?\n))((?:\2(?:\4|$))+)/gm,
  (m, l, w, x, br, t) => l + t.replace(/^(t)e(st)(?=\r?\n|$)/gm, (m, s, e) => s + x + e)
))
.as-console-wrapper.as-console-wrapper { max-height: 100vh }

Если известно, что переводы строк - только \n, то можно упростить:

var s = `test1
test
test
test
test

test2
test
test

test3
test
test
test
test
test
test`

console.log(s.replace(
  /^((test)(\d+)\n)((?:\2\n)+)/gm,
  (m, l, w, x, t) => l + t.replace(/^(t)e(st)$/gm, (m, s, e) => s + x + e)
))
.as-console-wrapper.as-console-wrapper { max-height: 100vh }

→ Ссылка