1

I’m struggling with this simple regex that is not working correctly in Safari:

(?<=\?.*)\?

It should match each ?, except of the first one.

I know that lookbehind is not working on Safari yet, but I need to find some workaround for it. Any suggestions?

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75

3 Answers3

2

You can use an alternation capture until the first occurrence of the question mark. Use that group again in the replacement to leave it unmodified.

In the second part of the alternation, match a questionmark to be replaced.

const regex = /^([^?]*\?)|\?/g;
const s = "test ? test ? test ?? test /";
console.log(s.replace(regex, (m, g1) => g1 ? g1 : "[REPLACE]"));
The fourth bird
  • 154,723
  • 16
  • 55
  • 70
  • Clever… but it only works for single characters, right? `[^?]` cannot be used for character sequences. Probably, it can be replaced by a lookahead somehow. – Sebastian Simon Jun 10 '21 at 15:06
  • @SebastianSimon It repeats a single character other than a `?` Do you want to exclude something else? – The fourth bird Jun 10 '21 at 15:08
  • _“It repeats a single character other than a `?`”_ — Yes, that’s what `[^?]*` does… but my concern was that if the OP wanted to replace e.g. every sequence `abc`, except the first, then `[^abc]*` wouldn’t make sense for that, i.e. “`[^?]` cannot be used for character _sequences_”. It looks like `const regex = /^(.*?\?)|\?/g;` works exactly the same way, but now it can be expanded to `/^(.*?abc)|abc/g`. – Sebastian Simon Jun 10 '21 at 15:33
  • @SebastianSimon Ah that is what you mean, in that case you can do that yes. But I used that because the `?` is in the question. I would make it `^(.*?\babc\b)|\babc\b` in that case. – The fourth bird Jun 10 '21 at 15:35
  • That looks very promising, thank you very much @Thefourthbird, but now I'm wondering how I could automate it even more - for e.g given parameter "?" could be different like "!" etc. But when I'm trying to create string literal / concatenation I'm failing, as it cannot properly inject the value of my variable. Any suggestions what am I doing wrong? – Bartek Słysz Jun 11 '21 at 08:00
  • 1
    @BartekSłysz In that case, you can use the RegExp constructor like `const param = "?"; const regex = new RegExp(\`^([^${param}]*\\${param})|\\${param}\`, 'g');` – The fourth bird Jun 11 '21 at 08:06
  • @Thefourthbird I just don't know how to thank you! You're the beast! – Bartek Słysz Jun 11 '21 at 08:24
  • Also note that the `?` is a meta characters and needs to be escaped in the regex `\?` to match it literally. Matching `!` does not have to be escaped. See [this page](https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript) which and how to escape the meta characters in Javascript when creating a dynamic pattern. – The fourth bird Jun 11 '21 at 08:28
  • 1
    Got it, accepted, and noted about ! mark. Thank you for tons of valuable advices :) – Bartek Słysz Jun 11 '21 at 08:37
  • @Thefourthbird can you show me this unedited comment here, as it seems like there is some mismatch with previous version of this regex: `const param = "?"; const regex = new RegExp(`^([^${param}]*\\${param})|\\${param}`, 'g');` – Bartek Słysz Jun 11 '21 at 08:58
  • @BartekSłysz You can see the regex pattern if you log it. See https://ideone.com/GySOjy The pattern that it creates is `^([^?]*\?)|\?` which is the same as in the answer. – The fourth bird Jun 11 '21 at 09:08
0

There are always alternatives to lookbehinds. In this case, all you need to do is replace all instances of a character (sequence), except the first.

The .replace method accepts a function as the second argument. That function receives the full match, each capture group match (if any), the offset of the match, and a few other things as parameters. .indexOf can report the first offset of a match. Alternatively, .search can also report the first offset of a match, but works with regexes.

The two offsets can be compared inside the function:

const yourString = "Hello? World? What? Who?",
    yourReplacement = "!",
    pattern = /\?/g,
    patternString = "?",
    firstMatchOffsetIndexOf = yourString.indexOf(patternString),
    firstMatchOffsetSearch = yourString.search(pattern);

console.log(yourString.replace(pattern, (match, offset) => {
  if(offset !== firstMatchOffsetIndexOf){
    return yourReplacement;
  }
  
  return match;
}));

console.log(yourString.replace(pattern, (match, offset) => {
  if(offset !== firstMatchOffsetSearch){
    return yourReplacement;
  }
  
  return match;
}));

This works for character sequences, too:

const yourString = "Hello. Hello. Hello. Hello.",
    yourReplacement = "Hi",
    pattern = /Hello/g,
    firstOffset = yourString.search(pattern);

console.log(yourString.replace(pattern, (match, offset) => {
  if(offset !== firstOffset){
    return yourReplacement;
  }
  
  return match;
}));
Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
0

Split and join with

var s = "one ? two ? three ? four"
var l = s.split("?")               // Split with ?
var first = l.shift()              // Get first item and remove from l
console.log(first + "?" + l.join("<REPLACED>")) // Build the results
Ryszard Czech
  • 18,032
  • 4
  • 24
  • 37