Javascript: отрицательный взгляд за эквивалентом?
Есть ли способ добиться эквивалента негативного взгляда в регулярных выражениях javascript? Мне нужно сопоставить строку, которая не начинается с определенного набора символов.
Кажется, я не могу найти регулярное выражение, которое делает это без сбоев, если совпадающая часть найдена в начале строки. Негативные взгляды, кажется, единственный ответ, но у javascript его нет.
РЕДАКТИРОВАТЬ: Это регулярное выражение, которое я хотел бы работать, но это не так:
(?<!([abcdefg]))m
Так что это будет соответствовать "м" в "Джим" или "м", но не "джем"
11 ответов
В 2018 году утверждения утверждений были приняты в спецификации ECMAScript. Это было реализовано в V8 и поставляется без флагов с Google Chrome v62 и в Node.js v6 за флагом и v9 без флага. Итак, если вы разрабатываете для среды Chrome (например, Electron) или Node, вы можете начать использовать lookbehinds уже сегодня!
Положительный взгляд за использование:
console.log(
"$9.99 €8.47".match(/(?<=\$)\d+(\.\d*)?/) // Matches "9.99"
);
Отрицательный взгляд за использование:
console.log(
"$9.99 €8.47".match(/(?<!\$)\d+(?:\.\d*)/) // Matches "8.47"
);
Поддержка на других платформах:
- Mozilla Firefox работает над этим: отслеживается здесь.
- Microsoft Edge тоже работает над этим: отслеживается здесь (голосовое предложение пользователя).
Поскольку Javascript поддерживает негативную перспективу, один из способов сделать это:
перевернуть строку ввода
совпадать с обратным регулярным выражением
отменить и переформатировать матчи
const reverse = s => s.split('').reverse().join('');
const test = (stringToTests, reversedRegexp) => stringToTests
.map(reverse)
.forEach((s,i) => {
const match = reversedRegexp.test(s);
console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
});
Пример 1:
После вопроса Эндрю Энсли:
test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)
Выходы:
jim true token: m
m true token: m
jam false token: Ø
Пример 2:
После комментария @neaumusic (соответствует max-height
но нет line-height
знаковое существо height
):
test(['max-height', 'line-height'], /thgieh(?!(-enil))/)
Выходы:
max-height true token: height
line-height false token: Ø
Предположим, вы хотите найти все int
не предшествует unsigned
:
С поддержкой негативного поиска:
(?<!unsigned )int
Без поддержки негативных взглядов:
((?!unsigned ).{9}|^.{0,8})int
По сути, идея состоит в том, чтобы захватить n предшествующих символов и исключить совпадение с отрицательным прогнозом, но также сопоставить случаи, когда нет предшествующих n символов. (где n - длина оглядки назад).
Итак, регулярное выражение в вопросе:
(?<!([abcdefg]))m
будет переводить на:
((?!([abcdefg])).|^)m
Возможно, вам придется поиграть с группами захвата, чтобы найти точное место строки, которая вас интересует, или вы хотите заменить определенную часть чем-то другим.
Стратегия Mijoja работает для вашего конкретного случая, но не в целом:
js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama
Вот пример, где цель состоит в том, чтобы соответствовать двойному l, но не если ему предшествует "ba". Обратите внимание на слово "balll" - истинный взгляд сзади должен был подавить первые 2 л, но соответствовать 2-й паре. Но, сопоставляя первые 2 л и затем игнорируя это совпадение как ложное срабатывание, механизм регулярных выражений исходит из конца этого совпадения и игнорирует все символы в ложном положительном результате.
Использование
newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
Вы можете определить группу без захвата, отрицая ваш набор символов:
(?:[^a-g])m
... который будет соответствовать каждому m
НЕ предшествует ни одна из этих букв.
Вот как я добился str.split(/(?<!^)@/)
для Node.js 8 (который не поддерживает lookbehind):
str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()
Работает? Да (Юникод не проверен). Неприятно? Да.
Как упоминалось ранее, JavaScript теперь позволяет просматривать назад. В старых браузерах вам все еще нужен обходной путь.
Бьюсь об заклад, что нет способа найти регулярное выражение без просмотра назад, которое дает точный результат. Все, что вы можете делать, это работать с группами. Предположим, у вас есть регулярное выражение(?<!Before)Wanted
, где Wanted
регулярное выражение, которое вы хотите сопоставить, и Before
это регулярное выражение, которое подсчитывает то, что не должно предшествовать совпадению. Лучшее, что вы можете сделать, это отрицать регулярное выражениеBefore
и используйте регулярное выражение NotBefore(Wanted)
. Желаемый результат - первая группа$1
.
В твоем случае Before=[abcdefg]
что легко отрицать NotBefore=[^abcdefg]
. Таким образом, регулярное выражение будет[^abcdefg](m)
. Если вам нужна должностьWanted
вы должны сгруппировать NotBefore
тоже, так что желаемый результат - вторая группа.
Если совпадения Before
узор имеет фиксированную длину n
, то есть, если шаблон не содержит повторяющихся токенов, вы можете избежать отрицания Before
шаблон и используйте регулярное выражение (?!Before).{n}(Wanted)
, но все равно придется использовать первую группу или регулярное выражение (?!Before)(.{n})(Wanted)
и воспользуйтесь второй группой. В этом примере шаблонBefore
на самом деле имеет фиксированную длину, а именно 1, поэтому используйте регулярное выражение (?![abcdefg]).(m)
или (?![abcdefg])(.)(m)
. Если вас интересуют все совпадения, добавьтеg
flag, см. мой фрагмент кода:
function TestSORegEx() {
var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
var reg = /(?![abcdefg])(.{1})(m)/gm;
var out = "Matches and groups of the regex " +
"/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
var match = reg.exec(s);
while(match) {
var start = match.index + match[1].length;
out += "\nWhole match: " + match[0] + ", starts at: " + match.index
+ ". Desired match: " + match[2] + ", starts at: " + start + ".";
match = reg.exec(s);
}
out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
+ s.replace(reg, "$1*$2*");
alert(out);
}
Следуя идее Mijoja и опираясь на проблемы, выявленные JasonS, у меня появилась эта идея; я немного проверил, но не уверен в себе, так что проверка кем-то более опытным, чем я, в js regex была бы отличной:)
var re = /(?=(..|^.?)(ll))/g
// matches empty string position
// whenever this position is followed by
// a string of length equal or inferior (in case of "^")
// to "lookbehind" value
// + actual value we would want to match
, str = "Fall ball bill balll llama"
, str_done = str
, len_difference = 0
, doer = function (where_in_str, to_replace)
{
str_done = str_done.slice(0, where_in_str + len_difference)
+ "[match]"
+ str_done.slice(where_in_str + len_difference + to_replace.length)
len_difference = str_done.length - str.length
/* if str smaller:
len_difference will be positive
else will be negative
*/
} /* the actual function that would do whatever we want to do
with the matches;
this above is only an example from Jason's */
/* function input of .replace(),
only there to test the value of $behind
and if negative, call doer() with interesting parameters */
, checker = function ($match, $behind, $after, $where, $str)
{
if ($behind !== "ba")
doer
(
$where + $behind.length
, $after
/* one will choose the interesting arguments
to give to the doer, it's only an example */
)
return $match // empty string anyhow, but well
}
str.replace(re, checker)
console.log(str_done)
мой личный вывод:
Fa[match] ball bi[match] bal[match] [match]ama
принцип заключается в том, чтобы позвонить checker
в каждой точке строки между любыми двумя символами, когда эта позиция является начальной точкой:
--- любая подстрока размером с то, что не требуется (здесь 'ba'
таким образом ..
) (если этот размер известен; в противном случае, возможно, сделать это будет сложнее)
--- --- или меньше, чем это, если это начало строки: ^.?
и, после этого,
--- что нужно искать (здесь 'll'
).
При каждом вызове checker
, будет тест, чтобы проверить, если значение до ll
это не то, что мы не хотим (!== 'ba'
); если это так, мы вызываем другую функцию, и она должна быть этой (doer
), который внесет изменения в str, если целью является эта, или, в более общем смысле, будет вводить во входные данные, необходимые для ручной обработки результатов сканирования str
,
здесь мы меняем строку, поэтому нам нужно было отслеживать разницу в длине, чтобы сместить места, заданные replace
, все рассчитано на str
, который сам по себе никогда не меняется.
поскольку примитивные строки неизменны, мы могли бы использовать переменную str
чтобы сохранить результат всей операции, но я подумал, что пример, уже усложненный заменами, будет понятнее с другой переменной (str_done
).
я думаю, что с точки зрения производительности это должно быть довольно резким: все эти бессмысленные замены "в", this str.length-1
время, плюс здесь ручная замена делающим, что означает много нарезки... вероятно, в этом конкретном вышеупомянутом случае, который можно сгруппировать, разрезая строку только один раз на части вокруг того места, где мы хотим вставить [match]
а также .join()
с этим [match]
сам.
Другое дело, что я не знаю, как он будет обрабатывать более сложные случаи, то есть сложные значения для поддельного вида сзади... длина, пожалуй, самая проблемная для получения данных.
И в checker
, в случае множественных возможностей нежелательных значений для $ позади, мы должны будем проверить его с помощью еще одного регулярного выражения (которое будет кэшировано (создано) снаружи). checker
Лучше всего избегать создания одного и того же объекта регулярного выражения при каждом вызове checker
) знать, стоит ли этого избегать.
надеюсь, я был чист; если не стесняйтесь, я постараюсь лучше.:)
Используя ваш случай, если вы хотите заменить m
с чем-то, например, преобразовать его в верхний регистр M
Можно отменить набор в группу захвата.
матч ([^a-g])m
, заменить $1M
"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam
([^a-g])
будет соответствовать любой символ не (^
) в a-g
диапазон и сохранить его в первой группе захвата, так что вы можете получить к нему доступ с $1
,
Итак, мы находим im
в jim
и заменить его на iM
что приводит к jiM
,
Это эффективно делает это
"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null
Пример поиска и замены
"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"
Обратите внимание, что отрицательная строка поиска должна быть длиной 1 символ, чтобы это работало.
Это может помочь, в зависимости от контекста:
Это соответствует m в jim, но не jam:
"jim jam".replace(/[a-g]m/g, "").match(/m/g)