patternMinor
replaceWithCallback() UDF
Viewed 0 times
replacewithcallbackudfstackoverflow
Problem
As explained in my blog article…
I was looking up the docs for Javascript's String replace()
function the other day (because, no, I could not remember the vagaries
of its syntax!). And whilst being relieved I had remembered it
correctly for my immediate requirements, I also noticed it can take a
callback instead of a string for its replacement argument. How cool is
that?
I figured this could be handy for CFML's
decided to look at how it would work before raising the enhancement
request, and in the process wrote a reasonable UDF which does the
business, by way of proof of concept.
I'll submit
self-approving stuff, so I'll get you lot to cast yer eyes over it
too, by way of community driven code review.
```
/**
@hint Analogous to reReplace()/reReplaceNoCase(), except the replacement is the result of a callback, not a hard-coded string
@string The string to process
@regex The regular expression to match
@callback A UDF which takes arguments match (substring matched), found (a struct of keys pos,len,substring, which is subexpression breakdown of the match), offset (where in the string the match was found), string (the string the match was found within)
@scope Number of replacements to make: either ONE or ALL
@caseSensitive Whether the regex is handled case-sensitively
*/
string function replaceWithCallback(required string string, required string regex, required any callback, string scope="ONE", boolean caseSensitive=true){
if (!isCustomFunction(callback)){ // for CF10 we could specify a type of "function", but not in CF9
throw(type="Application", message="Invalid callback argument value", detail="The callback argument of the replaceWithCallback() function must itself be a function reference.");
}
if (!isValid("regex", scope, "(?i)ONE|ALL")){
throw(type="Application", message="The scope argument of the
I was looking up the docs for Javascript's String replace()
function the other day (because, no, I could not remember the vagaries
of its syntax!). And whilst being relieved I had remembered it
correctly for my immediate requirements, I also noticed it can take a
callback instead of a string for its replacement argument. How cool is
that?
I figured this could be handy for CFML's
reReplace() function too, sodecided to look at how it would work before raising the enhancement
request, and in the process wrote a reasonable UDF which does the
business, by way of proof of concept.
I'll submit
replaceWithCallback() to CFLib, but I am wary ofself-approving stuff, so I'll get you lot to cast yer eyes over it
too, by way of community driven code review.
```
/**
@hint Analogous to reReplace()/reReplaceNoCase(), except the replacement is the result of a callback, not a hard-coded string
@string The string to process
@regex The regular expression to match
@callback A UDF which takes arguments match (substring matched), found (a struct of keys pos,len,substring, which is subexpression breakdown of the match), offset (where in the string the match was found), string (the string the match was found within)
@scope Number of replacements to make: either ONE or ALL
@caseSensitive Whether the regex is handled case-sensitively
*/
string function replaceWithCallback(required string string, required string regex, required any callback, string scope="ONE", boolean caseSensitive=true){
if (!isCustomFunction(callback)){ // for CF10 we could specify a type of "function", but not in CF9
throw(type="Application", message="Invalid callback argument value", detail="The callback argument of the replaceWithCallback() function must itself be a function reference.");
}
if (!isValid("regex", scope, "(?i)ONE|ALL")){
throw(type="Application", message="The scope argument of the
Solution
If the idea is to model this function on JavaScript's
Keeping in mind that I'm a complete newbie to CFML… I'd make a few minor tweaks to the loop.
By converting
When defining
In the following few lines, you can reuse
String.replace(), then there is a "bug". In JavaScript, when doing a global replace, the last argument passed to the callback is always the original unmodified string, not the string that may have had some of the replacements already applied to it, which is what you've written. The same should be true of the offsets passed to the callback: they should indicate the original positions.Keeping in mind that I'm a complete newbie to CFML… I'd make a few minor tweaks to the loop.
By converting
while (true) { … } to do { … } while (scope == "ALL");, you can avoid one of the inelegant break; statements.When defining
match, you can just reuse found.substring[1], and save a call to mid().In the following few lines, you can reuse
offset instead of found.pos[1] for a slight improvement in readability.var lengthDiff = 0;
var origString = string;
do {
var found = caseSensitive ? REFind(regex, string, startAt, true)
: REFindNoCase(regex, string, startAt, true);
if (!found.pos[1]) { // ie: it didn't find anything
break;
}
found.substring=[]; // as well as the usual pos and len that CF gives us, we're gonna pass the actual substrings too
for (var i=1; i <= arrayLen(found.pos); i++){
found.substring[i] = mid(string, found.pos[i], found.len[i]);
}
var match = found.substring[1];
var offset = found.pos[1];
var length = found.len[1];
var replacement = callback(match, found, offset - lengthDiff, origString);
string = removeChars(string, offset, length);
string = insert(replacement, string, offset-1);
lengthDiff += len(replacement) - length;
startAt = offset + len(replacement);
} while (scope == "ALL");Code Snippets
var lengthDiff = 0;
var origString = string;
do {
var found = caseSensitive ? REFind(regex, string, startAt, true)
: REFindNoCase(regex, string, startAt, true);
if (!found.pos[1]) { // ie: it didn't find anything
break;
}
found.substring=[]; // as well as the usual pos and len that CF gives us, we're gonna pass the actual substrings too
for (var i=1; i <= arrayLen(found.pos); i++){
found.substring[i] = mid(string, found.pos[i], found.len[i]);
}
var match = found.substring[1];
var offset = found.pos[1];
var length = found.len[1];
var replacement = callback(match, found, offset - lengthDiff, origString);
string = removeChars(string, offset, length);
string = insert(replacement, string, offset-1);
lengthDiff += len(replacement) - length;
startAt = offset + len(replacement);
} while (scope == "ALL");Context
StackExchange Code Review Q#28646, answer score: 5
Revisions (0)
No revisions yet.