Search code examples
iosobjective-cregex

Regex in Objective-C: how to replace matches with a dynamic template?


My input is like "Hi {{username}}", ie. a string with keywords to replace. However, the input is quite small (~ 10 keywords and 1000 characters total), and I have a million possible keywords stored in a hashtable data structure, each associated to its replacement.

Therefore, I do not want to iterate over the keyword list and try to replace each one in the input for obvious performance reason. I prefer to iterate only once over the input characters by looking for the regex pattern "\{\{.+?\}\}".

In Java, I make use of the Matcher.appendReplacement and Matcher.appendTail methods to do that. But I cannot find a similar API with NSRegularExpression.

private String replaceKeywords(String input)
{        
    Matcher m = Pattern.compile("\\{\\{(.+?)\\}\\}").matcher(input);
    StringBuffer sb = new StringBuffer();

    while (m.find())
    {
        String replacement = getReplacement(m.group(1));
        m.appendReplacement(sb, replacement);
    }

    m.appendTail(sb);
    return sb.toString();
}

Am I forced to implement such API myself, or did I miss something?


Solution

  • You can achieve this with NSRegularExpression:

    NSString *original = @"Hi {{username}} ... {{foo}}";
    NSDictionary *replacementDict = @{@"username": @"Peter", @"foo": @"bar"};
    
    NSString *pattern = @"\\{\\{(.+?)\\}\\}";
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
                                           options:0
                                         error:NULL];
    
    NSMutableString *replaced = [original mutableCopy];
    __block NSInteger offset = 0;
    [regex enumerateMatchesInString:original
                options:0
                  range:NSMakeRange(0, original.length)
                 usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        NSRange range1 = [result rangeAtIndex:1]; // range of the matched subgroup
        NSString *key = [original substringWithRange:range1];
        NSString *value = replacementDict[key];
        if (value != nil) {
            NSRange range = [result range]; // range of the matched pattern
            // Update location according to previous modifications:
            range.location += offset;
            [replaced replaceCharactersInRange:range withString:value];
            offset += value.length - range.length; // Update offset
        }
    }];
    NSLog(@"%@", replaced);
    // Output: Hi Peter ... bar
    

    A Swift version using the Regex type that was introduced with Swift 5.7, requires macOS 13+ or iOS 16+):

    import Foundation
    
    let original = "Hi {{username}} ... {{foo}}"
    let replacementDict = ["username" : "Peter", "foo" : "bar" ]
    
    var replaced = ""
    let regex = /\{\{(.+?)\}\}/
    var pos = original.startIndex
    
    // Look for next occurrence of {{key}}.
    while let match = try? regex.firstMatch(in: original[pos...]) {
        // Append original text preceding the match.
        replaced += original[pos..<match.range.lowerBound]
        
        // Replace {{key}} by corresponding value and append to the output.
        let key = String(match.1)
        if let value = replacementDict[key] {
            replaced += value
        } else {
            replaced += match.0
        }
    
        // Continue search after the current match.
        pos = match.range.upperBound
    }
    // Append original text after the final match.
    replaced += original[pos...]
    
    print(replaced)
    // Hi Peter ... bar