Aneel
Aneel

Reputation: 1433

Objective-C: efficient way to count objects in categories

I have a bunch of objects in a set of categories. I'd like to know how many of each category of there are.

In another language, I'd make a dictionary, then iterate over the objects, incrementing the appropriate value in the dictionary for each one. However, because I can't store a native numeric type in an NSDictionary in Objective-C, this has me constantly converting back and forth between NSNumber and a numeric type:

NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
for (MyObject *obj in objs) {  
    NSNumber *old = [dictionary objectForKey:obj.category];
    NSNumber *new = [NSNumber numberWithInteger:1 + old.integerValue];
    [dictionary setObject:new forKey:obj.category];
}

Is there a more efficient way to do this?

Upvotes: 2

Views: 174

Answers (4)

benzado
benzado

Reputation: 84338

You're looking for NSCountedSet.

NSCountedSet *bag = [[NSCountedSet alloc] init];
for (MyObject *obj in objs) {  
    [bag addObject:obj.category];
}
for (id category in bag) {
    NSLog(@"%d instances of %@", [bag countForObject:category], category);
}

Upvotes: 6

abarnert
abarnert

Reputation: 365925

If it really is a bottleneck, you can use the underlying CFDictionaryRef instead of NSMutableDictionary, to create a dictionary that stores integers directly, instead of boxing them in NSNumber values.

Read the documentation on CFDictionaryCreateMutable and CFDictionaryValueCallBacks for details, but the basic idea is that your retain and release do nothing, your description generates an NSNumber on the fly (or just does an stringWithFormat:"%d"), and your equal compares the ints directly.

Here's a sample showing the tricky parts of the code:

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>

CFStringRef intdesc(const void *value) {
  int i = (int)value;
  CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &i);
  CFStringRef s = CFCopyDescription(n);
  CFRelease(n);
  return s;
}

Boolean inteq(const void *value1, const void *value2) {
  int i1 = (int)value1, i2 = (int)value2;
  return i1 == i2;
}

int main(int argc, char *argv[]) {
  CFDictionaryValueCallBacks cb = { 0, NULL, NULL, &intdesc, &inteq };

  CFMutableDictionaryRef d = 
    CFDictionaryCreateMutable(NULL,
              0,
              &kCFTypeDictionaryKeyCallBacks,
              &cb);
  CFDictionarySetValue(d, @"Key1", (void *)1);
  CFDictionarySetValue(d, @"Key2", (void *)2);
  CFStringRef s = CFCopyDescription(d);
  NSLog(@"%@", s);
  CFRelease(s);
  CFRelease(d);
  return 0;
}

If you're gong to do a lot of this, you should probably wrap this up in ObjC (especially if you're using ARC), but that's left as an exercise for the reader.

Upvotes: 0

Phillip Mills
Phillip Mills

Reputation: 31016

If this turned out to be a big time-waster, you could:

  • create an array of all the unique categories
  • use its count to create a C-style array of int types
  • match the category of each incoming object, getting an array offset, and increment the corresponding int bucket
  • (optimize the category look-up as necessary, if that became a new bottle-neck)

But I certainly agree with the commenter who said, essentially, to measure first.

Upvotes: 0

David M. Syzdek
David M. Syzdek

Reputation: 15788

You could use NSMutableData to store your counters:

int                 * count;
NSMutableData       * num;
NSMutableDictionary * dictionary;

dictionary = [[NSMutableDictionary alloc] init];

for (MyObject *obj in objs)
{
    if ((count = [[dictionary objectForKey:obj.category] mutableBytes]) == nil)
    {
        num    = [NSMutableData dataWithCapacity:sizeof(int)];
        count  = [num mutableBytes];
        *count = 0;
        [dictionary setObject:num forKey:obj.category];
    };
    *count += 1;
};

This prevents the dictionary from being modified and a new NSNumber from being allocated each time a category's count is updated

Upvotes: 1

Related Questions