37

There is a substring that occurs in a string several times. I use rangeOfString, but it seems that it can only find the first location. How can I find all the locations of the substring?

NSString *subString1 = @"</content>";
NSString *subString2 = @"--\n";
NSRange range1 = [newresults rangeOfString:subString1];
NSRange range2 = [newresults rangeOfString:subString2];
int location1 = range1.location;
int location2 = range2.location;
NSLog(@"%i",location1);
NSLog(@"%i",location2);
yuji
  • 16,695
  • 4
  • 63
  • 64
forest
  • 411
  • 1
  • 4
  • 6

6 Answers6

84

You can use rangeOfString:options:range: and set the third argument to be beyond the range of the first occurrence. For example, you can do something like this:

NSRange searchRange = NSMakeRange(0,string.length);
NSRange foundRange;
while (searchRange.location < string.length) {
    searchRange.length = string.length-searchRange.location;
    foundRange = [string rangeOfString:substring options:0 range:searchRange];
    if (foundRange.location != NSNotFound) {
        // found an occurrence of the substring! do stuff here
        searchRange.location = foundRange.location+foundRange.length;
    } else {
        // no more substring to find
        break;
    }
}
catlan
  • 25,100
  • 8
  • 67
  • 78
PengOne
  • 48,188
  • 17
  • 130
  • 149
  • 6
    This assumes that substring appearances do not overlap, which is reasonable considering the question. In general it may not be a reasonable assumption. Consider the haystack "ababab". The needle "abab" appears at position 0 and at position 2 (overlapping the needle at position 0). Position 2 would be ignored by the above algorithm. – kevinlawler Mar 23 '13 at 05:23
  • 3
    Changing the text "+foundRange.length" to the text "+1" modifies the algorithm in the appropriate way. – kevinlawler Mar 23 '13 at 05:34
5

Swift 3.0

Find all locations of substring i

let text = "This is the text and i want to replace something"
let mutableAttributedString = NSMutableAttributedString(string: text)

var searchRange = NSRange(location: 0, length: text.characters.count)
var foundRange = NSRange()
while searchRange.location < text.characters.count {
    searchRange.length = text.characters.count - searchRange.location
    foundRange = (text as NSString).range(of: "i", options: NSString.CompareOptions.caseInsensitive, range: searchRange)
    if foundRange.location != NSNotFound {
        // found an occurrence of the substring! do stuff here
        searchRange.location = foundRange.location + foundRange.length
        mutableAttributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: foundRange)
    }
    else {
        // no more substring to find
        break
    }
}

//Apply
textLabel.attributedText = mutableAttributedString;

And this output-

enter image description here

Abhishek Jain
  • 4,557
  • 2
  • 32
  • 31
2

This is my solution. Basically, the algorithm traverses the string looking for substring matches and returns those matches in an array.

Since an NSRange is a struct it cannot be added to the array directly. By using NSValue, I can encode the match first and then add it to the array. To retrieve the range, I then decode the NSValue object to an NSRange.

#import <Foundation/Foundation.h>

NSRange makeRangeFromIndex(NSUInteger index, NSUInteger length) {
    return NSMakeRange(index, length - index);
}

NSArray<NSValue *> * allLocationsOfStringMatchingSubstring(NSString *text, NSString *pattern) {
    NSMutableArray *matchingRanges = [NSMutableArray new];
    NSUInteger textLength = text.length;
    NSRange match = makeRangeFromIndex(0, textLength);

    while(match.location != NSNotFound) {
        match = [text rangeOfString:pattern options:0L range:match];
        if (match.location != NSNotFound) {
            NSValue *value = [NSValue value:&match withObjCType:@encode(NSRange)];
            [matchingRanges addObject:value];
            match = makeRangeFromIndex(match.location + 1, textLength);
        }
    }

    return [matchingRanges copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *text = @"TATACCATGGGCCATCATCATCATCATCATCATCATCATCATCACAG";
        NSString *pattern = @"CAT";
        NSArray<NSValue *> *matches = allLocationsOfStringMatchingSubstring(text, pattern);

        NSLog(@"Text: %@", text);
        NSLog(@"Pattern: %@", pattern);
        NSLog(@"Number of matches found: %li", matches.count);

        [matches enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop) {
            NSRange match;
            [obj getValue:&match];
            NSLog(@"   Match found at index: %li", match.location);
        }];
    }
    return 0;
}
titusmagnus
  • 2,014
  • 3
  • 23
  • 23
  • `NSValue *value = [NSValue valueWithRange:match];` -- this looks easier than `value:withObjCType:` – OlDor Oct 10 '18 at 09:53
1

Passing nil to [string rangeOfString:substring options:nil range:searchRange]; shows a warning.

To get rid of the warning, put in an enum from this group

enum {
   NSCaseInsensitiveSearch = 1,
   NSLiteralSearch = 2,
   NSBackwardsSearch = 4,
   NSAnchoredSearch = 8,
   NSNumericSearch = 64,
   NSDiacriticInsensitiveSearch = 128,
   NSWidthInsensitiveSearch = 256,
   NSForcedOrderingSearch = 512,
   NSRegularExpressionSearch = 1024
};

https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/index.html#//apple_ref/doc/constant_group/Search_and_Comparison_Options

Gibtang
  • 90
  • 1
  • 8
1

Here is a version in Swift 2.2 of PengOne's answer with input from kevinlawler and Gibtang

Note: string and substring are of type NSString

let fullStringLength = (string as String).characters.count
var searchRange = NSMakeRange(0, fullStringLength)
while searchRange.location < fullStringLength {
    searchRange.length = fullStringLength - searchRange.location
    let foundRange = string.rangeOfString(substring as String, options: .CaseInsensitiveSearch, range: searchRange)
    if foundRange.location != NSNotFound {
        // found an occurrence of the substring! do stuff here
        searchRange.location = foundRange.location + 1
    } else {
        // no more strings to find
        break
    }
}
bwait
  • 29
  • 4
0

I suggest using regular expression because it's a more declarative way and has fewer lines of code to write.

NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"%@" options:nil error:nil];
NSString *toSearchStr = @"12312 %@ Text %@ asdsa %@";
__block int occurs = 0;
[regex enumerateMatchesInString:toSearchStr options:0 range:NSMakeRange(0, toSearchStr.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
       occurs++;
}];
// occurs == 3
karlbsm
  • 347
  • 4
  • 9