views:

54

answers:

3

I have a NSDictionary collection whose key is a unique id and value is an array with two different objects (FruitClass, ProductClass) and I would like to group the collection such that it's sorted first by ProductClass.productName and then by FruitClass.itemName.

So the final list would look something like:

{apple, butter}
{apple, pie}
{banana, daiquiri}
{banana, smoothie}
{melon, zinger}

where the first item is a FruitClass instance item and second is a ProductClass instance item.

What's the best way to go about doing this? Most of the examples I've come across are done on one key. How do you do it with an NSDictionary that has 2 different object types?

Looking at NSDictionary's's keysSortedByValueUsingSelector,

- (NSArray *)keysSortedByValueUsingSelector:(SEL)comparator

I get the impression that you would create the 'compare' method on the class type of the value object. So for multiple field sort, would I have to resort to creating a new object type, 'CombinedClass' which contains FruitClass & ProductClass and implement a 'compare' to make this happen?

FruitClass:
{
    NSString *itemName;
}
@end
@interface ProductClass
{
    NSString *productName;
}
@end
A: 

Your comparator can work on whatever you throw at it... you can make it treat its two arguments as NSArray objects, if this is what you need. When you put arrays as values into your dictionary, then just use those - no need for another class.

If you want to build a new class anyway (maybe for design reasons) - go for it, but it is not a "must do" here.

Edit: strike out to make clear that only one argument is given - as the other one is the object the selector is called on. Using NSArray will need a class extension, a custom class is much cleaner.

Eiko
The comparator is provided with only one argument, since the argument is being compared against `self`.
dreamlax
This is wrong. The comparator is a message that will be sent to the values of the dictionary. Since the values are arrays, they would have to be defined on the `NSArray` class.
St3fan
Sorry, one object. He can easily make it a class extension though - although a separate class is better design.
Eiko
A: 

You could add a category to NSArray that would do the comparison and you wouldn't have to create another class.

@interface NSArray ( MySortCategory )
    - (NSComparisonResult)customCompareToArray:(NSArray *)arrayToCompare;
@end

The implementation should be pretty straightforward based on your description.

Edit I got a little miffed that this was marked down with no comment, so I did a full implementation to make sure it would work. This is a little different than your sample, but the same idea.

FruitsAndProducts.h

@interface Fruit : NSObject
{
    NSString *itemName;
}

@property(nonatomic, copy)NSString *itemName;

@end

@interface Product : NSObject
{
    NSString *productName;
}

@property(nonatomic, copy)NSString *productName;

@end

FruitsAndProducts.m

#import "FruitsAndProducts.h"

@implementation Fruit

@synthesize itemName;

@end


@implementation Product

@synthesize productName;

@end

NSArray+MyCustomSort.h

@interface NSArray (MyCustomSort)
- (NSComparisonResult)customCompareToArray:(NSArray *)arrayToCompare;
@end

NSArray+MyCustomSort.m

#import "NSArray+MyCustomSort.h"

#import "FruitsAndProducts.h"

@implementation NSArray (MyCustomSort)

- (NSComparisonResult)customCompareToArray:(NSArray *)arrayToCompare
{
    // This sorts by product first, then fruit.
    Product *myProduct = [self objectAtIndex:0];
    Product *productToCompare = [arrayToCompare objectAtIndex:0];

    NSComparisonResult result = [myProduct.productName caseInsensitiveCompare:productToCompare.productName];
    if (result != NSOrderedSame) {
        return result;
    }

    Fruit *myFruit = [self objectAtIndex:1];
    Fruit *fruitToCompare = [arrayToCompare objectAtIndex:1];

    return [myFruit.itemName caseInsensitiveCompare:fruitToCompare.itemName];
}


@end

Here it is in action

// Create some fruit.
Fruit *apple = [[[Fruit alloc] init] autorelease];
apple.itemName = @"apple";
Fruit *banana = [[[Fruit alloc] init] autorelease];
banana.itemName = @"banana";
Fruit *melon = [[[Fruit alloc] init] autorelease];
melon.itemName = @"melon";

// Create some products
Product *butter = [[[Product alloc] init] autorelease];
butter.productName = @"butter";
Product *pie = [[[Product alloc] init] autorelease];
pie.productName = @"pie";
Product *zinger = [[[Product alloc] init] autorelease];
zinger.productName = @"zinger";

// create the dictionary. The array has the product first, then the fruit.
NSDictionary *myDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSArray arrayWithObjects:zinger, banana, nil], @"zinger banana", [NSArray arrayWithObjects:butter, apple, nil], @"butter apple", [NSArray arrayWithObjects:pie, melon, nil], @"pie melon", nil];

NSArray *sortedKeys = [myDict keysSortedByValueUsingSelector:@selector(customCompareToArray:)];

for (id key in sortedKeys) {
    NSLog(@"key: %@", key);
}
Robot K
Can someone please tell me why this is a bad idea? It's gotten two -1 scores and I have no idea why. I'm here to learn too.
Robot K
creating another data structure isn't bad per say but it feels like doing this way would mean one less full on class. Useful especially if I need to change the types used for sorting.
Justin Galzic
+1  A: 

If there is a data structure that consists of only one fruit and only one product then an array is not really a good option. You can use another class and provide a compare: comparator:

@interface ComboClass : NSObject
{
    FruitClass *fruit;
    ProductClass *product;
}

@property(nonatomic,retain) FruitClass *fruit;
@property(nonatomic,retain) ProductClass *product;

- initWithFruit:(FruitClass *)f andProduct:(ProductClass *) p;

@end


@implementation ComboClass

@synthesize fruit;
@synthesize product;

- (void) dealloc
{
    [fruit release];
    [product release];
    [super dealloc];
}

- initWithFruit:(FruitClass *)f andProduct:(ProductClass *) p
{
    self = [super init];
    if (!self) return nil;

    self.fruit = f;   // some recommend against accessor usage in -init methods
    self.product = p;

    return self;
}

- (NSComparisonResult) compare:(id) another
{
    NSComparisonResult result = [self.fruit.itemName compare:another.fruit.itemName];
    if (result == NSOrderedSame)
        return [self.product.productName compare:another.product.productName];
    else
        return result;
}

@end

Alternatively, you might be able to use an NSDictionary with product and fruit key-value pairs, (so you'll end up with dictionaries inside a dictionary). The NSSortDescriptor class can be used to sort arrays using values of key-paths, so it might be another option to explore.

dreamlax