I have an NSString or NSMutableString and would like to get the number of occurrences of a particular character.
I need to do this for quite a few characters -- uppercase English characters in this case -- so it would be nice for it to be quick.
I have an NSString or NSMutableString and would like to get the number of occurrences of a particular character.
I need to do this for quite a few characters -- uppercase English characters in this case -- so it would be nice for it to be quick.
replaceOccurrencesOfString:withString:options:range: will return the number of characters replaced in a NSMutableString.
[string replaceOccurrencesOfString:@"A"
withString:@"B"
options:NSLiteralSearch
range:NSMakeRange(0, [receiver length])];
Whenever you are looking for things in a NSString, try using NSScanner first.
NSString *yourString = @"ABCCDEDRFFED"; // For example
NSScanner *scanner = [NSScanner scannerWithString:yourString];
NSCharacterSet *charactersToCount = [NSCharacterSet characterSetWithCharactersInString:@"C"]; // For example
NSString *charactersFromString;
if (!([scanner scanCharactersFromSet:charactersToCount
intoString:&charactersFromString])) {
// No characters found
NSLog(@"No characters found");
}
// should return 2 for this
NSInteger characterCount = [charactersFromString length];
Performance comparison for the different Objective-C solutions.
Assume that all the methods below are NSString extensions (inside @implementation NSString (CountOfOccurrences)).
As a sample, I've used a random generated string of length 100000000 using all Latin characters (CharacterSet(charactersIn: "\u{0020}"..."\u{036F}") in Swift). And the character to count was @"a".
Tests performed on Xcode 10.3 on Simulator in release configuration.
There are two ways to count for a character: using NSLiteralSearch or not. The count will be different and the performance will be fundamentally affected. For fastest results, we will perform exact character-by-character equivalence. Below four solutions give very close performance results.
Using replaceOccurrencesOfString:withString:options:range:. This is the fastest solution in all scenarios: even if you replace NSLiteralSearch with kNilOptions, you're still faster than pierrot3887 scanner solution.
- (NSUInteger)countOccurrencesOfString:(NSString *)stringToFind
{
return [[NSMutableString stringWithString:self] replaceOccurrencesOfString:stringToFind
withString:stringToFind
options:NSLiteralSearch
range:NSMakeRange(0, self.length)];
}
Using stringByReplacingOccurrencesOfString:withString:options:range:.
- (NSUInteger)countOccurrencesOfString:(NSString *)stringToFind
{
NSString *strippedString = [self stringByReplacingOccurrencesOfString:stringToFind
withString:@""
options:NSLiteralSearch
range:NSMakeRange(0, self.length)];
return (self.length - strippedString.length) / stringToFind.length;
}
Using CFStringGetCharacterFromInlineBuffer.
See https://stackoverflow.com/a/15947190/1033581.
Using rangeOfString:options:range:.
- (NSUInteger)countOccurrencesOfString:(NSString *)stringToFind
{
//assert(stringToFind.length);
NSUInteger count = 0;
NSRange searchRange = NSMakeRange(0, self.length);
NSRange foundRange;
while ((void)(foundRange = [self rangeOfString:stringToFind options:NSLiteralSearch range:searchRange]), foundRange.length) {
count += 1;
NSUInteger loc = NSMaxRange(foundRange);
searchRange = NSMakeRange(loc, self.length - loc);
}
return count;
}
The below solutions do not use NSLiteralSearch and do not perform exact character-by-character equivalence. The first two are maybe 10 times slower than the fast solutions, and the last one is maybe 100 times slower.
Using scanUpToString:intoString:. Too bad that NSScanner doesn't offer an option for exact character-by-character equivalence.
- (NSUInteger)countOccurrencesOfString:(NSString *)stringToFind
{
NSScanner *scanner = [NSScanner scannerWithString:self];
scanner.charactersToBeSkipped = nil;
scanner.caseSensitive = YES;
NSUInteger numberOfOccurrences = 0;
while (!scanner.isAtEnd) {
[scanner scanUpToString:stringToFind intoString:nil];
if (!scanner.isAtEnd) {
numberOfOccurrences++;
[scanner scanString:stringToFind intoString:nil];
}
}
return numberOfOccurrences;
}
Using componentsSeparatedByString:. Regarding the argument of doable in one line, note that the fastest solution given above is also a one liner.
- (NSUInteger)countOccurrencesOfString:(NSString *)stringToFind
{
return [self componentsSeparatedByString:stringToFind].count - 1;
}
Using enumerateSubstringsInRange:options:usingBlock:.
- (NSUInteger)countOccurrencesOfCharacter:(NSString *)characterToFind
{
__block NSUInteger counter = 0;
[self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
if ([characterToFind isEqualToString:substring]) counter += 1;
}];
return counter;
}
I would probably use
NSString rangeOfCharacterFromSet:
or
rangeOfCharacterFromSet:options:range::
where the set is the set of characters you're searching for. It returns with the location of first character matching the set. Keep array or dictionary and increment the count for character, then repeat.
The example with the Scanner was crashing on iPhone. I found this solution :
NSString *yourString = @"ABCCDEDRFFED"; // For example
NSScanner *mainScanner = [NSScanner scannerWithString:yourString];
NSString *temp;
NSInteger numberOfChar=0;
while(![mainScanner isAtEnd])
{
[mainScanner scanUpToString:@"C" intoString:&temp];
numberOfChar++;
[mainScanner scanString:@"C" intoString:nil];
}
It worked for me without crash. Hope it can help !