Опережающие и ретроспективные проверки

В некоторых случаях нам нужно найти соответствие шаблону, за которым следует другой шаблон. Например, мы хотим получить цену одной индейки в строке: 1 индейка стоит 30€.

Нам нужно число (здесь предположим, что в цена – целое число, без десятичной точки), после которого следует знак валюты .

Именно для таких задач и существует опережающая проверка.

Опережающая проверка

Синтаксис: x(?=y) Пояснение: найди х при условии, что за ним следует y.

Для целого числа, за которым идёт знак , шаблон регулярного выражения будет \d+(?=€):

let str = "1 индейка стоит 30€";

alert( str.match(/\d+(?=€)/) ); // 30 (число 1 было проигнорировано, так как за ним НЕ следует `subject:€`)

Допустим, нам нужно узнать количество индеек, которое можно купить за 30€ – это число, за которым НЕ следует знак .

Для этой задачи мы можем применить негативную опережающую проверку.

Синтаксис: x(?!y) Пояснение: найди такой х, за которым НЕ следует y.

let str = "2 индейки стоят 60€";

alert( str.match(/\d+(?!€)/) ); // 2 (в этот раз была проигнорирована цена)

Ретроспективная проверка

Опережающие проверки позволяют задавать условия на то, что «идёт после».

Ретроспективная проверка выполняет такую же функцию, но с просмотром назад. Другими словами, она находит соответствие шаблону, только если перед ним есть что-то заранее определённое.

Синтаксис:

  • Позитивная ретроспективная проверка: (?<=y)x, выдаёт совпадение на x при условии, что перед ним ЕСТЬ y.
  • Негативная ретроспективная проверка: (?<!y)x, выдаёт совпадение на x при условии, что перед ним НЕТ y.

Чтобы протестировать ретроспективную проверку, давайте поменяем валюту на доллары США. Знак доллара обычно ставится перед суммой денег, поэтому для того чтобы найти $30, мы используем (?<=\$)\d+ – число, перед которым идёт $:

let str = "1 индейка стоит $30";

alert( str.match(/(?<=\$)\d+/) ); // 30 (одинокое число игнорируется)

Если нам необходимо найти количество индеек – число, перед которым не идёт $, мы можем использовать негативную ретроспективную проверку (?<!\$)\d+:

let str = "2 индейки стоят $60";

alert( str.match(/(?<!\$)\d+/) ); // 2 (проигнорировалась цена)

Захват групп

Как правило, то что находится внутри скобок, задающих опережающую и ретроспективную проверку, не включается в результат совпадения.

Например, в шаблоне \d+(?=€) знак не будет включён в результат. Это логично, ведь мы ищем число \d+, а (?=€) – это всего лишь проверка, что за ним идёт знак .

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

В следующем примере знак валюты (€|kr) будет включён в результат вместе с суммой:

let str = "1 индейка стоит 30€";
let reg = /\d+(?=(€|kr))/; // добавлены дополнительные скобки вокруг €|kr

alert( str.match(reg) ); // 30, €

Тоже самое можно применить к ретроспективной проверке:

let str = "1 индейка стоит $30";
let reg = /(?<=(\$|£))\d+/;

alert( str.match(reg) ); // 30, $

Обратите внимание, что порядок выдачи результата ретроспективной проверки остаётся прежним, хотя скобки из опережающей проверки расположены ПЕРЕД основным шаблоном.

Обычно совпадения с выражениями в скобках нумеруются по порядку – слева направо. Однако, ретроспективная проверка является исключением, так как при ней совпадение с выражением в скобках всегда идёт после результата основного шаблона. Так, в нашем примере совпадение с основным шаблоном \d+ будет идти первым, а результат для (\$|£) будет вторым.

Итого

Опережающая и ретроспективная проверки удобны, когда мы хотим искать шаблон по дополнительному условию на контекст, в котором он находится.

Для простых регулярных выражений мы можем сделать похожую вещь «вручную». То есть, найти все совпадения, независимо от контекста, а затем в цикле отфильтровать подходящие.

Как мы помним, что str.matchAll и reg.exec возвращают совпадения со свойством .index, поэтому мы знаем их точное расположение в тексте и можем посмотреть на контекст. Но обычно регулярные выражения удобнее.

Виды проверок:

Паттерн Тип Совпадение
x(?=y) Позитивная опережающая x, если за ним следует y
x(?!y) Негативная опережающая x, если за ним НЕ следует y
(?<=y)x Позитивная ретроспективная x, если следует за y
(?<!y)x Негативная ретроспективная x, если НЕ следует за y

Опережающая проверка также может быть использована, чтобы отключить возврат при поиске. Для чего нам это может понадобиться и другие детали, вы узнаете в следующей главе.

Задачи

Есть строка с целыми числами.

Создайте регулярное выражениие, которое ищет только неотрицательные (ноль разрешён).

Пример использования:

let reg = /ваше регулярное выражение/g;

let str = "0 12 -5 123 -18";

alert( str.match(reg) ); // 0, 12, 123

Регэксп для целого числа: \d+.

Мы можем исключить отрицательные добавлением негативной ретроспективной проверки: (?<!-)\d+.

Однако, если попробуем применить такой регэксп, то увидим лишний результат:

let reg = /(?<!-)\d+/g;

let str = "0 12 -5 123 -18";

console.log( str.match(reg) ); // 0, 12, 123, 8

Как видите, оно находит 8 из -18. Чтобы исключить его, надо убедиться, что регэксп не будет искать число с середины другого (неподходящего) числа.

Мы можем сделать это добавлением ещё одной проверки: (?<!-)(?<!\d)\d+. Теперь (?<!\d) гарантирует, что поиск не начнётся после цифры.

Можем объединить проверки в одну:

let reg = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(reg) ); // 0, 12, 123
Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.