Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Negative lookbehind equivalent in JavaScript

People also ask

What is a negative Lookbehind?

A negative lookbehind assertion asserts true if the pattern inside the lookbehind is not matched.

Does SED support Lookbehind?

I created a test using grep but it does not work in sed . This works correctly by returning bar . I was expecting footest as output, but it did not work. sed does not support lookaround assertions.

What is lookahead and Lookbehind?

Lookahead allows to add a condition for “what follows”. Lookbehind is similar, but it looks behind. That is, it allows to match a pattern only if there's something before it.

What is Lookbehind?

Unlike look-ahead, look-behind is used when the pattern appears before a desired match. You're “looking behind” to see if a certain string of text has the desired pattern behind it. If it does, then that string of text is a match.


Since 2018, Lookbehind Assertions are part of the ECMAScript language specification.

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

Answer pre-2018

As Javascript supports negative lookahead, one way to do it is:

  1. reverse the input string

  2. match with a reversed regex

  3. reverse and reformat the matches


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]) : 'Ø');
  });

Example 1:

Following @andrew-ensley's question:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

Outputs:

jim true token: m
m true token: m
jam false token: Ø

Example 2:

Following @neaumusic comment (match max-height but not line-height, the token being height):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

Outputs:

max-height true token: height
line-height false token: Ø

Lookbehind Assertions got accepted into the ECMAScript specification in 2018.

Positive lookbehind usage:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);

Negative lookbehind usage:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);

Platform support:

  • ✔️ V8
    • ✔️ Google Chrome 62.0
    • ✔️ Microsoft Edge 79.0
    • ✔️ Node.js 6.0 behind a flag and 9.0 without a flag
    • ✔️ Deno (all versions)
  • ✔️ SpiderMonkey
    • ✔️ Mozilla Firefox 78.0
  • 🛠️ JavaScriptCore: Apple is working on it
    • 🛠️ Apple Safari
    • 🛠️ iOS WebView (all browsers on iOS + iPadOS)
  • ❌ Chakra: Microsoft was working on it but Chakra is now abandoned in favor of V8
    • ❌ Internet Explorer
    • ❌ Edge versions prior to 79 (the ones based on EdgeHTML+Chakra)

Let's suppose you want to find all int not preceded by unsigned:

With support for negative look-behind:

(?<!unsigned )int

Without support for negative look-behind:

((?!unsigned ).{9}|^.{0,8})int

Basically idea is to grab n preceding characters and exclude match with negative look-ahead, but also match the cases where there's no preceeding n characters. (where n is length of look-behind).

So the regex in question:

(?<!([abcdefg]))m

would translate to:

((?!([abcdefg])).|^)m

You might need to play with capturing groups to find exact spot of the string that interests you or you want to replace specific part with something else.


Mijoja's strategy works for your specific case but not in general:

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

Here's an example where the goal is to match a double-l but not if it is preceded by "ba". Note the word "balll" -- true lookbehind should have suppressed the first 2 l's but matched the 2nd pair. But by matching the first 2 l's and then ignoring that match as a false positive, the regexp engine proceeds from the end of that match, and ignores any characters within the false positive.


Use

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});

You could define a non-capturing group by negating your character set:

(?:[^a-g])m

...which would match every m NOT preceded by any of those letters.


This is how I achieved str.split(/(?<!^)@/) for Node.js 8 (which doesn't support lookbehind):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

Works? Yes (unicode untested). Unpleasant? Yes.