Gavin Hope
Gavin Hope

Reputation: 2193

Sorting NSArray, NSComparator block vs. NSSortDescriptor, with null values

I've been using the block based approach to sort an NSArray... However, I'd noticed a sort-related bug so started to investigate.

Background: I'm dealing with an NSArray of EKReminder objects, which have a creationDate property. I want to sort the reminders by descending creationDate (newest reminders, first).

This was my previous code:

// NSArray* fetchedReminders... contents pulled from reminder calendars...

NSArray* sortedArray = [fetchedReminders sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
    NSDate* first = [(EKReminder*)a creationDate];
    NSDate* second = [(EKReminder*)b creationDate];
    return [second compare:first];
}];

That code, I believe, is correct. However, I ended up with some reminders in the database that had null as their creation date. This introduced a bug - the resulting sort was incorrect. The null values were neither at the beginning or the end, and it seems that having nulls in the array messed with this comparison approach, as many of the reminders were out of sequence.

NSSortDescriptor

So, I tried swapping out the block-based approach in favour of sortedArrayUsingDescriptors. Here's the current code:

// NSArray* fetchedReminders... contents pulled from reminder calendars...

NSSortDescriptor* sortDescriptor;
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"creationDate" ascending:NO];
NSArray* sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray* sortedArray = [fetchedReminders sortedArrayUsingDescriptors:sortDescriptors];

This works.

(With the current data, 101 reminders, 6 of them have null creation dates and they're all placed at the end. The other reminders are in the correct sequence).

Questions

First, am I doing anything wrong with the sortedArrayUsingComparator approach?

If not, is it expected that these different approaches handle nulls differently?

In any case, does that make the NSSortDescriptor approach the preferred method, if you can potentially have nulls in your data?

Upvotes: 1

Views: 1036

Answers (1)

Itai Ferber
Itai Ferber

Reputation: 29918

The core issue that you're running into here is that you're not manually handling null values in the block you pass to sortedArrayUsingComparator:. Depending on which values the block gets called with, first can be nil, second can be nil, or both can be nil.

Any message sent to nil returns the equivalent 0 value (e.g. a method returning a float, when sent to nil returns 0.0f, a method returning int when sent to nil returns 0, and a method returning an object sent to nil returns nil). This means that you have the following cases:

This means that as the call is being made across the values in the array, some values are being returned that are non-sensical (e.g. [nil compare:[NSDate date]] returns 0, equivalent to NSOrderedSame, which is clearly not true), not to mention the results returned by undefined calls.

In effect, these invalid values are being sorted into strange places in your array. If you had some defined behavior for what should happen if either value is nil, you'd get consistent behavior.

The following code makes the sorting behavior consistent (and should give you similar behavior to the sort descriptors method above):

NSArray* sortedArray = [fetchedReminders sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
    NSDate* first = [(EKReminder*)a creationDate];
    NSDate* second = [(EKReminder*)b creationDate];
    if (!first && !second) {
        // nils have the same relative ordering
        return NSOrderedSame;
    } else if (!first) {
        // second is not nil; send first toward the end of the array
        return NSOrderedDescending;   
    } else if (!second) {
        // first is not nil; send second toward the end of the array
        return NSOrderedAscending;
    } else {
        // Neither is nil; this is valid
        return [second compare:first];
    }
}];

Upvotes: 3

Related Questions