Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an Objective-c regex replace with callback/C# MatchEvaluator equivalent?

I have a C# project I'm intending to port to Objective-C. From what I understand about Obj-C, it looks like there's a confusing variety of Regex options but I can't see anything about a way of doing a replace with callback.

I'm looking for something that is the equivalent of the C# MatchEvaluator delegate or PHP's preg_replace_callback. An example of what I want to do in C# is -

// change input so each word is followed a number showing how many letters it has

string inputString = "Hello, how are you today ?";
Regex theRegex = new Regex(@"\w+");

string outputString = theRegex.Replace(inputString, delegate (Match thisMatch){
   return thisMatch.Value + thisMatch.Value.Length;
});

// outputString is now 'Hello5, how3 are3 you3 today5 ?'

How could I do this in Objective-C ? In my actual situation the Regex has both lookahead and lookbehind assertions in it though, so any alternative involving finding the strings in advance and then doing a series of straight string replaces won't work unfortunately.

like image 955
Michael Low Avatar asked Oct 18 '10 06:10

Michael Low


2 Answers

Foundation has a NSRegularExpression class (iOS4 and later), which may be useful to you. From the docs:

The fundamental matching method for NSRegularExpression is a Block iterator method that allows clients to supply a Block object which will be invoked each time the regular expression matches a portion of the target string. There are additional convenience methods for returning all the matches as an array, the total number of matches, the first match, and the range of the first match.

For example:

NSString *input = @"Hello, how are you today?";

// make a copy of the input string. we are going to edit this one as we iterate
NSMutableString *output = [NSMutableString stringWithString:input];

NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression 
                                regularExpressionWithPattern:@"\\w+"
                                                     options:NSRegularExpressionCaseInsensitive 
                                                       error:&error];

// keep track of how many additional characters we've added (1 per iteration)
__block NSUInteger count = 0;  

[regex enumerateMatchesInString:input
                        options:0
                          range:NSMakeRange(0, [input length])
                     usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

    // Note that Blocks in Objective C are basically closures
    // so they will keep a constant copy of variables that were in scope
    // when the block was declared
    // unless you prefix the variable with the __block qualifier

    // match.range is a C struct
    // match.range.location is the character offset of the match
    // match.range.length is the length of the match        

    NSString *matchedword = [input substringWithRange:match.range];

    // the matched word with the length appended
    NSString *new  = [matchedword stringByAppendingFormat:@"%d", [matchedword length]];

    // every iteration, the output string is getting longer
    // so we need to adjust the range that we are editing
    NSRange newrange = NSMakeRange(match.range.location+count, match.range.length);
    [output replaceCharactersInRange:newrange withString:new];

    count++;
}];
NSLog(@"%@", output); //output: Hello5, how3 are3 you3 today5?
like image 175
atshum Avatar answered Nov 18 '22 17:11

atshum


I modified atshum's code to make it a bit more flexible:

__block int prevEndPosition = 0;
[regex enumerateMatchesInString:text
                        options:0
                          range:NSMakeRange(0, [text length])
                     usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop)
{
    NSRange r = {.location = prevEndPosition, .length = match.range.location - prevEndPosition};

    // Copy everything without modification between previous replacement and new one
    [output appendString:[text substringWithRange:r]]; 
    // Append string to be replaced
    [output appendString:@"REPLACED"];

    prevEndPosition = match.range.location + match.range.length;
}];

// Finalize string end
NSRange r = {.location = prevEndPosition, .length = [text length] - prevEndPosition};
[output appendString:[text substringWithRange:r]];

Seems to work for now (probably needs a bit more testing)

like image 23
Benjamin Piette Avatar answered Nov 18 '22 16:11

Benjamin Piette