0

I have a string which looks like

var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42)
  .ramBam(8.1, 0).bam(8.1, (slot_height-thick)/2)

I want to put a tag around the last .bam() or .ramBam().

str.replace(/(\.(ram)?bam\(.*?\))$/i, '<span class="focus">$1</span>');

And I hope to get:

new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42).ramBam(8.1, 0)<span class="focus">.bam(8.1, (slot_height-thick)/2)</span>

But somehow I keep on fighting with the non greedy parameter, it wraps everything after new Bammer with the span tags. Also tried a questionmark after before the $ to make the group non greedy.

I was hoping to do this easy, and with the bam or ramBam I thought that regex would be the easiest solution but I think I'm wrong.

Where do I go wrong?

3
  • do you want to include the (number, number) in the tag around .bam and .ramBam? Commented Mar 10, 2020 at 20:51
  • @FujiRoyale yes, in fact there maybe nested params in there. Commented Mar 11, 2020 at 8:17
  • There is a bit of a catch to this question which I omitted to mention is that there maybe nested params ie: ram(0,(slotheight-5)/2) Commented Mar 11, 2020 at 8:22

3 Answers 3

2

You can use the following regex:

(?!.*\)\.)(\.(?:bam|ramBam)\(.*\))$

Demo

(?!.*\)\.)         # do not match ').' later in the string
(                  # begin capture group 1                   
  .\               # match '.'
  (?:bam|ramBam)   # match 'bam' or 'ramBam' in non-cap group
  \(.*\)           # match '(', 0+ chars, ')'
)                  # end capture group 1
$                  # match end of line

For the example given in the question the negative lookahead (?!.*\)\.) moves an internal pointer to just before the substring:

.bam(8.1, (slot_height-thick)/2)

as that is the first location where there is no substring ). later in the string.

If there were no end-of-line anchor $ and the string ended:

...0).bam(8.1, (slot_height-thick)/2)abc

then the substitution would still be made, resulting in a string that ends:

...0)<span class="focus">.bam(8.1, (slot_height-thick)/2)</span>abc

Including the end-of-line anchor prevents the substitution if the string does not end with the contents of the intended capture group.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your answer, and contributions to the previous answer. My apologies for letting out the requirement of nested parameters. Booboo's answer also covers that, so I will make that the accepted answer for now.
2

Regex to use:

/\.((?:ram)?[bB]am\([^)]*\))(?!.*\.(ram)?[bB]am\()/
  1. \. Matches period.
  2. (?:ram)? Optionally matches ram in a non-capturing group.
  3. [bB]am Matches bam or Bam.
  4. \( Matches (.
  5. [^)]* Matches 0 or more characters as long as they are not a ).
  6. ) Matches a ). Items 2. through 6. are placed in Capture Group 1.
  7. (?!.*\.(ram)?[bB]am\() This is a negative lookahead assertion stating that the rest of the string contains no further instance of .ram( or .rambam( or .ramBam( and therefore this is the last instance.

See Regex Demo

let str = 'var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, 0).bam(0, -42).ramBam(8.1, 0).bam(8.1, slot_height)';
console.log(str.replace(/\.((?:ram)?[bB]am\([^)]*\))(?!.*\.(ram)?[bB]am\()/, '<span class="focus">.$1</span>'));

Update

The JavaScript regular expression engine is not powerful enough to handle nested parentheses. The only way I know of solving this is if we can make the assumption that after the final call to bam or ramBam there are no more extraneous right parentheses in the string. Then where I had been scanning the parenthesized expression with \([^)]*\), which would fail to pick up final parentheses, we must now use \(.*\) to scan everything until the final parentheses. At least I know no other way. But that also means that the way that I had been using to determine the final instance of ram or ramBam by using a negative lookahead needs a slight adjustment. I need to make sure that I have the final instance of ram or ramBam before I start doing any greedy matches:

(\.(?:bam|ramBam)(?!.*\.(bam|ramBam)\()\((.*)\))

See Regex Demo

  1. \. Matches ..
  2. (?:bam|ramBam) Matches bam or ramBam.
  3. (?!.*\.(bam|ramBam)\() Asserts that Item 1. was the final instance
  4. \( Matches (.
  5. (.*) Greedily matches everything until ...
  6. \) the final ).
  7. ) Items 1. through 6. are placed in Capture Group 1.

let str = 'var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42) .ramBam(8.1, 0).bam(8.1, (slot_height-thick)/2)';
console.log(str.replace(/(\.(?:bam|ramBam)(?!.*\.(bam|ramBam)\()\((.*)\))/, '<span class="focus">$1</span>'));

2 Comments

@CarySwoveland Of course, now (?:ram|ramBam) seems so more logical. Oh well.
Thanks for your answer, there is small catch however: there maybe nested params in the string. Which does exclude the last ) from the match. For now I will revert to str.substring, str.lastIndexOf juggling.
1

The non-greedy flag isn't quite right here, as that will just make the regex select the minimal number of characters to fit the pattern. I'd suggest that you do something with a negative lookahead like this:

str.replace(/(\.(?:ram)?[Bb]am\([^)]*\)(?!.*(ram)?[Bb]am))/i, '<span class="focus">$1</span>');

Note that this will only replace the last function name (bam OR ramBam), but not both. You'd need to take a slightly different approach to be able to replace both of them.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.