NSSortDescriptor: Custom comparison on multiple keys simultaneously

11,015

Solution 1

You could sort with a block instead:

NSArray *sortedArray;
sortedArray = [myArray sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
    MyObject *first = (MyObject*)a;
    MyObject *second = (MyObject*)b;

    if (first.endCalYear < second.endCalYear) {
        return NSOrderedAscending;
    }
    else if (first.endCalYear > second.endCalYear) {
        return NSOrderedDescending;
    }
    // endCalYear is the same

    if (first.endMonth < second.endMonth) {
        return NSOrderedAscending;
    }
    else if (first.endMonth > second.endMonth) {
        return NSOrderedDescending;
    }    
    // endMonth is the same

    if (first.periodLength < second.periodLength) {
        return NSOrderedAscending;
    }
    else if (first.periodLength > second.periodLength) {
        return NSOrderedDescending;
    }
    // periodLength is the same

    return NSOrderedSame;
}]

This sorts by endCalYear ascending, then endMonth ascending and finally periodLength ascending. You could modify it to change the order or switch the signs in the if statement to make it descending.

For NSFetchedResultsController you might want to try something else:

It looks like you can pass it a list of descriptors, one for each column that you want sorted:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSSortDescriptor *descriptor1 = [[NSSortDescriptor alloc] initWithKey:@"endCalYear" ascending:YES];
NSSortDescriptor *descriptor2 = [[NSSortDescriptor alloc] initWithKey:@"endMonth" ascending:YES];
NSSortDescriptor *descriptor3 = [[NSSortDescriptor alloc] initWithKey:@"periodLength" ascending:YES];
NSArray *sortDescriptors = @[descriptor1, descriptor2, descriptor3];
[fetchRequest setSortDescriptors:sortDescriptors];

Solution 2

APIs for sorting are generally capable to use an array of NSSortDescriptors, not just one, so why not use them?

For example, NSArray has a method called sortedArrayUsingDescriptors: (note the plural form) which takes an array of NSSortDescriptor objects.

So you can simply write this:

NSSortDescriptor *endCalYearSD = [NSSortDescriptor sortDescriptorWithKey:@"endCalYear" ascending:YES];
NSSortDescriptor *endMonthSD = [NSSortDescriptor sortDescriptorWithKey:@"endMonth" ascending:YES];
NSSortDescriptor *periodLenSD = [NSSortDescriptor sortDescriptorWithKey:@"periodLength" ascending:YES];

NSArray *sortedArray = [originalArray sortedArrayUsingDescriptors:@[endCalYearSD, endMonthSD, periodLenSD]];

This way, your originalArray will be sorted first by endCalYear, each entry with the same endCalYear will be sorted by endMonth, and each entry with same endCalYear and endMonth will then be sorted by periodLendth.

You have APIs that use an array of sortDescriptors for most of the APIs that propose sorting (including CoreData and such) so the principle is the same all the time.


If you really need to stick with only one NSSortDescriptor (and your sorting algorithm isn't flexible enough to use a block-based comparator or an array of NSSortDescriptors), you can simply provide a property to your custom object that compute some value on which you can base your sorting algorithm.

For example add such method to your custom class:

-(NSUInteger)sortingIndex {
    return endCalYear*10000 + endCalMonth*100 + periodLength;
}

Then sort on this key/property. This is not really very clean to read and a very pretty design pattern, but the better way would be to alter your sorting algorithm API to allow sorting on multiple keys at once, so…


[EDIT] (to answer your [EDIT] on block-based API)

I don't see why the block-based API sortDescriptorWithKey:ascending:comparator: won't work for you. You can specify whatever custom NSComparator block you need there, so this block can tell, given two objects, which one is before the other. The way you determine which one is before which one is up to you, you can compare only endCalYear, or endCalYear and endMonth, etc. so there is no limitation here about sorting using multiple keys.

Share:
11,015
AlexR
Author by

AlexR

Updated on June 16, 2022

Comments

  • AlexR
    AlexR almost 2 years

    I have an custom object which contains time period based information, for instance the attributes endCalYear, endMonth and periodLength which indicate the end of each period and its length.

    I would like to create an NSSortDescriptor based or other sorting method which combines these three attributes and allows sorting this object on all three keys at the same time.

    Example:

    EndCalYear  endMonth  periodLength (months) sortOrder
    2012        6         6                     1
    2012        6         3                     2
    2012        3         3                     3
    2011        12        12                    4
    

    The sort algorithm would be completely discretionary based on my own algorithm.

    How could I code such an algorithm?

    The block based sortDescriptorWithKey:ascending:comparator: method won't work in my view because it would allow me to specify only one sorting key. However, I need to sort on all three keys at the same time.

    Any ideas or thoughts on how to solve this?

    Thank you!

  • AlexR
    AlexR over 11 years
    Brilliant, Nathan! Thank you very much! Would it be possible to apply the same sort logic to a NSFetchedResultsController?
  • AlexR
    AlexR over 11 years
    Unfortunately I can't use the NSSortDescriptors array for multiple keys because my proprietary sorting algorithm needs to evaluate all three keys at the same time, for instance if ((first.endCalYear < second.endCalYear) && (first.periodLength < second.periodLength)) {...}. It is not about sorting on endCalYear first, then endMonth, then periodLength.
  • AlexR
    AlexR over 11 years
    Unfortunately I can't use the NSSortDescriptors array for multiple keys because my proprietary sorting algorithm needs to evaluate all three keys at the same time, for instance if ((first.endCalYear < second.endCalYear) && (first.periodLength < second.periodLength)) {...}. It is not about sorting on endCalYear first, then endMonth, then periodLength.
  • Nathan Villaescusa
    Nathan Villaescusa over 11 years
    In that case I don't think there is a way that you can do the type of sorting you want in the NSFetchedResultsController. That class is used to manage Core Data and the sorting is done at the database level.
  • AliSoftware
    AliSoftware over 11 years
    If you have a proprietary sorting algorithm, I don't really get why do you need an NSSortDescriptor? Are you sure using it is appropriate? Why not provide a block API (using NSComparator blocks) instead (and your proprietary sorting algorithm only have to call the block on two objects to know which one is before the other, so whatever your algorithm is, adding such an API should be possible)?
  • AliSoftware
    AliSoftware over 11 years
    Anyway, if you really need to stick with only one NSSortDescriptor (and your sorting algorithm isn't flexible enough to use a block-based comparator or an array of NSSortDescriptors), you can simply provide a property to your custom object that compute some value on which you can base your sorting algorithm. For example add a method -(NSUInteger)sortingIndex { return endCalYear*10000 + endCalMonth*100 + periodLength; } and sort on this key/property.
  • AliSoftware
    AliSoftware over 11 years
    I've edited my answer to add some considerations about using the block-based API and the way it is compatible with working with multiple keys