Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace a Regex capture group with uppercase in Javascript

I'd like to know how to replace a capture group with its uppercase in JavaScript. Here's a simplified version of what I've tried so far that's not working:

> a="foobar"
'foobar'
> a.replace( /(f)/, "$1".toUpperCase() )
'foobar'
> a.replace( /(f)/, String.prototype.toUpperCase.apply("$1") )
'foobar'

Would you explain what's wrong with this code?

like image 379
NO WAR WITH RUSSIA Avatar asked May 26 '11 17:05

NO WAR WITH RUSSIA


3 Answers

You can pass a function to replace.

var r = a.replace(/(f)/, function(v) { return v.toUpperCase(); });

Explanation

a.replace( /(f)/, "$1".toUpperCase())

In this example you pass a string to the replace function. Since you are using the special replace syntax ($N grabs the Nth capture) you are simply giving the same value. The toUpperCase is actually deceiving because you are only making the replace string upper case (Which is somewhat pointless because the $ and one 1 characters have no upper case so the return value will still be "$1").

a.replace( /(f)/, String.prototype.toUpperCase.apply("$1"))

Believe it or not the semantics of this expression are exactly the same.

like image 68
ChaosPandion Avatar answered Nov 18 '22 15:11

ChaosPandion


I know I'm late to the party but here is a shorter method that is more along the lines of your initial attempts.

a.replace('f', String.call.bind(a.toUpperCase));

So where did you go wrong and what is this new voodoo?

Problem 1

As stated before, you were attempting to pass the results of a called method as the second parameter of String.prototype.replace(), when instead you ought to be passing a reference to a function

Solution 1

That's easy enough to solve. Simply removing the parameters and parentheses will give us a reference rather than executing the function.

a.replace('f', String.prototype.toUpperCase.apply)

Problem 2

If you attempt to run the code now you will get an error stating that undefined is not a function and therefore cannot be called. This is because String.prototype.toUpperCase.apply is actually a reference to Function.prototype.apply() via JavaScript's prototypical inheritance. So what we are actually doing looks more like this

a.replace('f', Function.prototype.apply)

Which is obviously not what we have intended. How does it know to run Function.prototype.apply() on String.prototype.toUpperCase()?

Solution 2

Using Function.prototype.bind() we can create a copy of Function.prototype.call with its context specifically set to String.prototype.toUpperCase. We now have the following

a.replace('f', Function.prototype.apply.bind(String.prototype.toUpperCase))

Problem 3

The last issue is that String.prototype.replace() will pass several arguments to its replacement function. However, Function.prototype.apply() expects the second parameter to be an array but instead gets either a string or number (depending on if you use capture groups or not). This would cause an invalid argument list error.

Solution 3

Luckily, we can simply substitute in Function.prototype.call() (which accepts any number of arguments, none of which have type restrictions) for Function.prototype.apply(). We have now arrived at working code!

a.replace(/f/, Function.prototype.call.bind(String.prototype.toUpperCase))

Shedding bytes!

Nobody wants to type prototype a bunch of times. Instead we'll leverage the fact that we have objects that reference the same methods via inheritance. The String constructor, being a function, inherits from Function's prototype. This means that we can substitute in String.call for Function.prototype.call (actually we can use Date.call to save even more bytes but that's less semantic).

We can also leverage our variable 'a' since it's prototype includes a reference to String.prototype.toUpperCase we can swap that out with a.toUpperCase. It is the combination of the 3 solutions above and these byte saving measures that is how we get the code at the top of this post.

like image 24
Joshua Piccari Avatar answered Nov 18 '22 14:11

Joshua Piccari


Why don't we just look up the definition?

If we write:

a.replace(/(f)/, x => x.toUpperCase())

we might as well just say:

a.replace('f','F')

Worse, I suspect nobody realises that their examples have been working only because they were capturing the whole regex with parentheses. If you look at the definition, the first parameter passed to the replacer function is actually the whole matched pattern and not the pattern you captured with parentheses:

function replacer(match, p1, p2, p3, offset, string)

If you want to use the arrow function notation:

a.replace(/xxx(yyy)zzz/, (match, p1) => p1.toUpperCase()
like image 17
Bernhard Wagner Avatar answered Nov 18 '22 14:11

Bernhard Wagner