String replace regex match's capture group unless a different regex matches the same capture group in JS

Viewed 386

This is about the simplest example I can come up with (and actually not a far cry from the actual use case) that I anticipate people might not tell me to simply change the regex itself, but here goes.

Say I have these lovely nonsensical input words: sent seimk semek t͡ʃeno eint͡ɬi em t͡ʃeʃeimp t͡ɬent͡ɬien keinsen

And I want to search replace all instances of ei OR e with a before n or m, which (ei|e)(?:n|m) matches, UNLESS

  1. it is directly preceded (i.e. no negative lookbehinds, since they'll cause a false negative) by t͡ɬ, t͡ʃ, or d͡ʒ, so i.e. (?:t͡ɬ|t͡ʃ|d͡ʒ)(ei|e), or

  2. if the n/m is directly succeeded (i.e. no negative lookaheads, for the same reason) by p, t, or k, i.e. if (ei|e)(?:n|m)(?:p|t|k) matches

The desired output is therefore sent seimk samek t͡ʃeno ant͡ɬi am t͡ʃeʃeimp t͡ɬent͡ɬian kansan.

So if the "find and replace" function in JS is String.replace(RegExp pattern, String replacement), then then you would have to compress all 3 regexes into 1 for the first argument, which 1) I don't think is possible? It would require negative non-capturing groups which... aren't a thing, right? and 2) in the actual use case, the regex patterns are generated by a text parser, not by hand, and I don't have enough faith in my ability to write a parser smart enough to optimize.

The other way I thought about doing this was to simply cache all the matches to the first regex in a dictionary, and then remove everything that should be ruled out by the other two, but when you look at the output:

let sEnv = "(ei|e)(?:n|m)";
let reEnv = new RegExp(sEnv, "g");  

let sExc1 = "(?:t͡ɬ|t͡ʃ|d͡ʒ)(ei|e)";
let reExc1 = new RegExp(sExc1, "g");

let sExc2 = "(ei|e)(?:n|m)(?:p|t|k)";
let reExc2 = new RegExp(sExc2, "g");

let sWords = "sent seimk semek t͡ʃeno eint͡ɬi em t͡ʃeʃeimp t͡ɬent͡ɬien keinsen";
let tWords = sWords.split(" ");
for (i = 0; i < tWords.length; i++){
    let sCurrentWord = tWords[i];
    let result;
    let tMatches = {}
    // first cache all the matches (ignoring the exceptions)
    while (result = reEnv.exec(sCurrentWord)) {
    tMatches[result.index] = result[0].length;
    }
    // then remove the exceptions
    while (result = reExc1.exec(sCurrentWord)) {
    delete tMatches[result.index];
    }
    while (result = reExc2.exec(sCurrentWord)) {
    delete tMatches[result.index];
    }
    // then apply all remaining matches
    let sOutput = sCurrentWord;
    for (var index in tMatches){
    
        console.log(sCurrentWord+": starting at "+index+", "+tMatches[index]+" chars long");
    }

}

It's... a bit confused. Not only is it apparently capturing the n despite being in a non-capturing group (result length printed keeps being 2 when the thing I want to capture is only 1 char long), but it also filtered out eint͡ɬi but kept t͡ʃeno as matches, which is the opposite of what it's supposed to do - and as the length of the input list grows this method must get quite slow.

How else can I go about this find-and-replace?

1 Answers

You actually have more hidden requirement here, namely the p, t, k cannot be followed with a diacritic mark.

Your main mistake is that you think that lookarounds cannot be used to match locations immediately preceded/followed with some pattern. In fact, lookarounds DO and ALWAYS DO match locations that are immediately preceded/followed with some patterns.

In your case, you can use (assuming you are using the ECMAScript 2018 compliant RegExp):

text = text.replace(/(?<!t͡ɬ|t͡ʃ|d͡ʒ)(ei?)(?=[nm](?![ptk](?!\p{M})))/gu, 'a')

See the regex demo. Details:

  • (?<!t͡ɬ|t͡ʃ|d͡ʒ) - a negative lookbehind that fails the match if there is t͡ɬ, t͡ʃ or d͡ʒ immediately to the left of the current location
  • (ei?) - ei or e
  • (?=[nm](?![ptk](?!\p{M}))) - a positive lookahead that matches a location that is immediately followed with n or m that are not immediately followed with p, t nor k that are not immediately followed with any diacrtic mark (\p{M}).

See the JavaScript demo:

const regex = /(?<!t͡ɬ|t͡ʃ|d͡ʒ)(ei?)(?=[nm](?![ptk](?!\p{M})))/gu;
const text = 'sent seimk semek t͡ʃeno eint͡ɬi em t͡ʃeʃeimp t͡ɬent͡ɬien keinsen';
console.log(text.replace(regex, 'a'));

Related