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?


Solution 1:

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.

Solution 2:

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.