39

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.

jscs
  • 63,694
  • 13
  • 151
  • 195
Elliot
  • 6,086
  • 11
  • 45
  • 57

10 Answers10

101

You can do this in one line. For example, this counts the number of spaces:

NSUInteger numberOfOccurrences = [[yourString componentsSeparatedByString:@" "] count] - 1;
Robert
  • 37,670
  • 37
  • 171
  • 213
gbaor
  • 1,419
  • 3
  • 11
  • 19
26

Try this category on NSString:

@implementation NSString (OccurrenceCount)

- (NSUInteger)occurrenceCountOfCharacter:(UniChar)character
{
    CFStringRef selfAsCFStr = (__bridge CFStringRef)self;

    CFStringInlineBuffer inlineBuffer;
    CFIndex length = CFStringGetLength(selfAsCFStr);
    CFStringInitInlineBuffer(selfAsCFStr, &inlineBuffer, CFRangeMake(0, length));

    NSUInteger counter = 0;

    for (CFIndex i = 0; i < length; i++) {
        UniChar c = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, i);
        if (c == character) counter += 1;
    }

    return counter;
}

@end

This one is approximately 5 times faster than the componentsSeparatedByString: approach.

Jacque
  • 820
  • 1
  • 9
  • 15
  • This category works fine, but I have a question: can you use both UniChar (from CFString.h) and unichar (from NSString) as being the same? – Bjinse Mar 17 '14 at 12:56
  • @Bjinse both of them are typedef to `unsigned short`, so yes. – Cœur Sep 10 '19 at 03:36
16

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])];
jscs
  • 63,694
  • 13
  • 151
  • 195
CynicismRising
  • 940
  • 4
  • 5
7

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];
Cœur
  • 37,241
  • 25
  • 195
  • 267
Abizern
  • 146,289
  • 39
  • 203
  • 257
  • i have not been able to get this to work at all. I'm trying to count the number of spaces. – Lawrence Dec 24 '09 at 20:15
  • 3
    @lawrence By default, spaces and whitespace are ignored by NSScanner. – borrrden Dec 14 '12 at 02:33
  • 4
    You can call `setCharactersToBeSkipped:(NSCharacterSet *)skipSet` with `nil` as the `skipSet` - and the `NSScanner` won't skip any characters. – Gavin Hope May 05 '14 at 14:43
  • This won't return `2`, because `scanCharactersFromSet:intoString:` will stop as soon as it doesn't get a match. – Cœur Sep 10 '19 at 09:30
6

Nowadays the first thing that come to my mind for something like that: NSCountedSet

NSString *string = @"AAATTC";

NSMutableArray *array = [NSMutableArray array];

[string enumerateSubstringsInRange:NSMakeRange(0, [string length]) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
    [array addObject:substring];
}] ;
NSCountedSet * set = [[NSCountedSet alloc] initWithArray:array];

for (NSString *nucleobase in @[@"C", @"G", @"A", @"T"]){
    NSUInteger count = [set countForObject:nucleobase];
    NSLog(@"%@: %lu", nucleobase, (unsigned long)count);
}

logs:

C: 1
G: 0
A: 3
T: 2
Cœur
  • 37,241
  • 25
  • 195
  • 267
vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
5

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.

Fast solutions (exact character-by-character equivalence)

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.

1. Fastest solution: an adaptation of CynicismRising answer.

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)];
}

2. Second fastest, another adaptation of CynicismRising answer.

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;
}

3. Third fastest, Jacque solution.

Using CFStringGetCharacterFromInlineBuffer. See https://stackoverflow.com/a/15947190/1033581.

4. Fourth fastest, a conversion of my Swift answer to Objective-C.

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;
}

Slow solutions

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.

5. Slow solution: adaptation of pierrot3887 answer

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;
}

6. Slower solution: gbaor solution

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;
}

7. Slowest solution: adaptation of vikingosegundo answer

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;
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
2

Your solution did not work for me, I added a condition in the loop to increment numberOfChar only if mainScanner has reached the end of the string :

NSString *yourString = @"ABCCDEDRFFED"; // For example
NSScanner *mainScanner = [NSScanner scannerWithString:yourString];
NSString *temp;
NSInteger numberOfChar=0;
while(![mainScanner isAtEnd])
{
   [mainScanner scanUpToString:@"C" intoString:&temp];
   if(![mainScanner isAtEnd]) {
      numberOfChar++;
      [mainScanner scanString:@"C" intoString:nil];
   }
}

Note that this is a quick fix, I don't have time to make an elegant solution...

1

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 !

1

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.

stefanB
  • 77,323
  • 27
  • 116
  • 141
  • If I'm understanding the Documentation correctly, this would give the range of any of the characters in the set. But I need the count of *each* character. – Elliot Jun 02 '09 at 06:57
  • 1
    my idea is to keep dictionary of char -> count pairs and then get the char at given index and increment it's count in dictionary ... or you could just iterate over the string and check if each character is in your set, if it is then increment it's count – stefanB Jun 02 '09 at 07:28
0

Here is a Swift 3 working version, for NSRange, Range, String and NSString! Enjoy :)

/// All ranges using NSString and NSRange
/// Is usually used together with NSAttributedString

extension NSString {
    public func ranges(of searchString: String, options: CompareOptions = .literal, searchRange: NSRange? = nil) -> [NSRange] {
        let searchRange = searchRange ?? NSRange(location: 0, length: self.length)
        let subRange = range(of: searchString, options: options, range: searchRange)
        if subRange.location != NSNotFound {

            let nextRangeStart = subRange.location + subRange.length
            let nextRange = NSRange(location: nextRangeStart, length: searchRange.location + searchRange.length - nextRangeStart)
            return [subRange] + ranges(of: searchString, options: options, searchRange: nextRange)
        } else {
            return []
        }
    }
}

/// All ranges using String and Range<Index>
/// Is usually used together with NSAttributedString

extension String {
    public func ranges(of searchString: String, options: CompareOptions = [], searchRange: Range<Index>? = nil ) -> [Range<Index>] {
        if let range = range(of: searchString, options: options, range: searchRange, locale: nil) {

            let nextRange = range.upperBound..<(searchRange?.upperBound ?? endIndex)
            return [range] + ranges(of: searchString, searchRange: nextRange)
        } else {
            return []
        }
    }
}
txaidw
  • 485
  • 4
  • 13