Replace a Regex capture group with uppercase in Javascript

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.


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.


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()

Old post but it worth to extend @ChaosPandion answer for other use cases with more restricted RegEx. E.g. ensure the (f) or capturing group surround with a specific format /z(f)oo/:

> a="foobazfoobar"
'foobazfoobar'
> a.replace(/z(f)oo/, function($0,$1) {return $0.replace($1, $1.toUpperCase());})
'foobazFoobar'
// Improve the RegEx so `(f)` will only get replaced when it begins with a dot or new line, etc.

I just want to highlight the two parameters of function makes finding a specific format and replacing a capturing group within the format possible.