Help sorting an NSArray across two properties (with NSSortDescriptor?)
Asked Answered
C

6

9

I'm a bit of a NSSortDescriptor n00b. I think, though, it is the right tool for what I need to do:

I have an NSArray consisting of objects with keys, say, "name" and "time". Instead of verbalizing it, here's an example:

input:

name: time
B: 4
C: 8
B: 5
C: 4
A: 3
C: 2
A: 1
A: 7
B: 6


desired output:

name: time
A: 1 <---
A: 3
A: 7
C: 2 <---
C: 4
C: 8
B: 4 <---
B: 5
B: 6

So the values are sorted by "time" and grouped by "name". A comes first because he had the smallest time value, and all values for A come after one another. Then comes C, he had the second smallest time value out of all his values. I have indicated the values that determine how the names are sorted; within each name group, sorting is by time.

How do I get from input to output NSArray in the most efficient way? (cpu- and memory-wise, not necessarily code-wise.) How would I construct the NSSortDescriptors for this, or use any other method? I don't want to roll my own unless it's the most efficient way.

Camber answered 3/2, 2010 at 7:16 Comment(0)
A
18

The sortedArrayUsingDescriptors: NSArray method does most of what you need:

The first descriptor specifies the primary key path to be used in sorting the receiver’s contents. Any subsequent descriptors are used to further refine sorting of objects with duplicate values. See NSSortDescriptor for additional information.

Some filtering with NSPredicate is required too:

NSSortDescriptor *timeSD = [NSSortDescriptor sortDescriptorWithKey: @"time" ascending: YES];

NSMutableArray *sortedByTime = [UnsortedArray sortedArrayUsingDescriptors: timeSD];
NSMutableArray *sortedArray = [NSMutableArray arrayWithCapacity:[sortedByTime count]];

while([sortedByTime count]) 
{
        id groupLead = [sortedByTime objectAtIndex:0];  
        NSPredicate *groupPredicate = [NSPredicate predicateWithFormat:@"name = %@", [groupLead name]];

        NSArray *group = [sortedByTime filteredArrayUsingPredicate: groupPredicate];

        [sortedArray addObjectsFromArray:group];
        [sortedByTime removeObjectsInArray:group];
}

I have no idea if this is the most efficient method, but until you have reason to believe that it is causing problems there's no need to worry the performance implications. It's premature optimisation. I wouldn't have any concerns about the performance of this method. You've got to trust the framework otherwise you'll end up rewriting it (thus undermine the point of the framework) due to an unfounded paranoia.

Alignment answered 3/2, 2010 at 12:12 Comment(6)
This does not answer the question: my situation is more complicated than simply sorting by name.Camber
@Jaanus. Ohhh I see. I didn't notice that the order of the groups is dependent on the time.Alignment
@Jaanus. I've updated the code so that it actually answer the question!Alignment
Thanks, this looks like what I need. I will try out several approaches from the answers and report back.Camber
Upvoted the answer. Wish I could deduct a quarter point for the performance implications comment though. It's not premature optimization to consider the efficiency of your algorithms. That phrase is getting tossed around more and more, and this is not what it's about. Taking the time to consider the complexity of your algorithms, and whether there's a better way, is just good engineering.Tilla
Now it has some bug.Rhabdomancy
B
21

My solution is:

    NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"time" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, sortDescriptor2, nil];

You can try it

Burble answered 24/12, 2010 at 11:7 Comment(1)
That's resolved my issues, flexible to add more sortDescriptor.Amends
A
18

The sortedArrayUsingDescriptors: NSArray method does most of what you need:

The first descriptor specifies the primary key path to be used in sorting the receiver’s contents. Any subsequent descriptors are used to further refine sorting of objects with duplicate values. See NSSortDescriptor for additional information.

Some filtering with NSPredicate is required too:

NSSortDescriptor *timeSD = [NSSortDescriptor sortDescriptorWithKey: @"time" ascending: YES];

NSMutableArray *sortedByTime = [UnsortedArray sortedArrayUsingDescriptors: timeSD];
NSMutableArray *sortedArray = [NSMutableArray arrayWithCapacity:[sortedByTime count]];

while([sortedByTime count]) 
{
        id groupLead = [sortedByTime objectAtIndex:0];  
        NSPredicate *groupPredicate = [NSPredicate predicateWithFormat:@"name = %@", [groupLead name]];

        NSArray *group = [sortedByTime filteredArrayUsingPredicate: groupPredicate];

        [sortedArray addObjectsFromArray:group];
        [sortedByTime removeObjectsInArray:group];
}

I have no idea if this is the most efficient method, but until you have reason to believe that it is causing problems there's no need to worry the performance implications. It's premature optimisation. I wouldn't have any concerns about the performance of this method. You've got to trust the framework otherwise you'll end up rewriting it (thus undermine the point of the framework) due to an unfounded paranoia.

Alignment answered 3/2, 2010 at 12:12 Comment(6)
This does not answer the question: my situation is more complicated than simply sorting by name.Camber
@Jaanus. Ohhh I see. I didn't notice that the order of the groups is dependent on the time.Alignment
@Jaanus. I've updated the code so that it actually answer the question!Alignment
Thanks, this looks like what I need. I will try out several approaches from the answers and report back.Camber
Upvoted the answer. Wish I could deduct a quarter point for the performance implications comment though. It's not premature optimization to consider the efficiency of your algorithms. That phrase is getting tossed around more and more, and this is not what it's about. Taking the time to consider the complexity of your algorithms, and whether there's a better way, is just good engineering.Tilla
Now it has some bug.Rhabdomancy
T
3

I would create a new class called ItemGroup, and then add an extra ivar called group to your item class:

@interface ItemGroup : NSObject
{
    NSNumber * time;
}
@property (nonatomic, copy) time;
@end

@interface ItemClass : NSobject
{
    NSString * name;
    NSNumber * time;
    ItemGroup * group;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSNumber * time;
@property (nonatomic, assign) ItemClass * group; // note: must be assign
@end

Then, you could do the following:

NSMutableDictionary * groups = [NSMutableDictionary dictionaryWithCapacity:0];
for (ItemClass * item in sourceData)
{
    ItemGroup * group = [groups objectForKey:item.name];
    if (group == nil)
    {
        group = [[ItemGroup alloc] init];
        [groups setObject:group forKey:item.name];
        [group release];

        group.time = item.time;
    }
    else if (item.time < group.time)
    {
        group.time = item.time;
    }
    item.group = group;
}

This code loops through the unsorted array, keeping track of the minimum time for each group, and also setting the group for each item. With that complete, you simply sort on group.time and time:

NSSortDescriptor * groupSorter;
groupSort = [NSSortDescriptor sortDescriptorWithKey:@"group.time" ascending:YES];

NSSortDescriptor * timeSorter;
timeSort = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];

NSArray * sortDescriptors = [NSArray arrayWithObjects:groupSort, timeSort, nil];

NSArray * sorted = [sourceData sortedArrayUsingDescriptors:sortDescriptors];

And that should do the trick!

UPDATE: Note that you could get much better performance if you were able to assign the groups straight out of the gate. Something like this:

@interface ItemGroup : NSObject
{
    NSString * name;
    NSNumber * time;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSSNumber * time;
@end

@interface ItemClass : NSObject
{
    ItemGroup * group;
    NSNumber * time;
}
@property (nonatomic, retain) ItemGroup * group;
@property (nonatomic, copy) NSNumber * time;
@end

Now, if you maintain a list of groups somewhere (they could even go in an array somewhere, if need be):

ItemGroup * group_A = [[ItemGroup alloc] init];
group_A.name = @"A";
ItemGroup * group_B = [[ItemGroup alloc] init];
group_B.name = @"B";
...

And instead of setting the names of your data items, you set their group:

someItem.group = group_A;
someItem.time = GetSomeRandomTimeValue();
[sourceData addObject:someItem];
....

This would greatly simplify the loop used to set group times:

for (ItemClass * item in sourceData)
{
    if (item.time < group.time) { group.time = item.time; }
}

And, if you really wanted to be blazing fast about it, you could even modify the property setter for your time property to set the group times on the fly:

@implementation ItemClass
- (void)setTime:(NSNumber *)newTime
{
    if (newTime < group.time) { group.time = newTime; }
    time = [newTime copy];
}
@end

Note that you would have to be sure that group had been set before you set the time. With this in place, you wouldn't need that sorting loop at all. The sortDescriptors would be enough.

Tribesman answered 3/2, 2010 at 7:24 Comment(9)
I understand this, but I don't think this answers the question. I am not sorting by name, I need to rank and group the names based on what's the smallest time value for a given name.Camber
Ah. Now I see. That's much more interesting.Tribesman
Do you define the type of object that gets stored in the original array? i.e. is it a custom class that you can add ivars to?Tribesman
Yes, it is my custom class and I could add ivars. I can't see myself how that could help though. The data is volatile and new values may arrive at runtime. At any given time, I just have a snapshot of the data I need to sort this way.Camber
Out of all the answers so far, I like these the best, especially the groups approach, will report back. I think this has O(n) complexity, much better than my current naive O(n*n). Assigning out of the gate would be great, but the actual situation is more complicated and depends on the environment that is not known at save time.Camber
How are you getting the original list of data? Do you start with an empty array, add items (name and time) on the fly, sort the results and then empty the array and start over, or do items get randomly added and removed from the array during runtime?Tribesman
The original list gets loaded from a Core Data backend. Additional entries will arrive at runtime from network, and their "time" is not current time, but rather, when they were created in some other system.Camber
Hmm. Yes, that makes it difficult to set the groups ahead of time. I'm sure there is still a way to do it, but it is probably not worth the effort. How did the testing go?Tribesman
Might not be able to test before the weekend, will comment when I get to it.Camber
H
1

I went through to make a little code (didn't try running it or really go over it so there might be a couple of mistakes, but it has the general idea) to do what you're looking for. Performance wise, it probably won't be the best if you start running into huge amounts of data. I'm sure there's a better way to do this, but I felt like doing it the most basic way as a "temporary fix" answer.

NSMutableArray *copiedarray = [YourFirstArray mutableCopy];
NSMutableArray *sortedarray = [[NSMutableArray alloc] init];
NSMutableArray *tempgroup = nil;
NSSortDescriptor * groupSorter = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];

NSInteger i;
NSInteger savedlowest = -1;
NSString *savedname = @"";


while ([copiedarray count] > 0) {
    ///reset lowest time and group
    savedlowest = -1;
    savedname = @"";

    ///grab the lowest time and group name
    for (ii = 0;ii < [copiedarray count]; ii++) {
        if (savedlowest==-1 || ((YourClass *)([copiedarray objectAtIndex:ii])).time<savedlowest)) {
            savedname = ((YourClass *)([copiedarray objectAtIndex:ii])).name;
            savedlowest = ((YourClass *)([copiedarray objectAtIndex:ii])).time;
        }
    }

    //we have the lowest time and the type so we grab all those items from the group
    tempgroup = [[NSMutableArray alloc] init];
    for (ii = [copiedarray count]-1;ii > -1; ii--) {
        if ([((YourClass *)([copiedarray objectAtIndex:ii])).name isEqualToString:savedname]) {
            ///the item matches the saved group so we'll add it to our temporary array
            [tempgroup addObject:[copiedarray objectAtIndex:ii]];
            ///remove it from the main copied array for "better performance"
            [copiedarray removeObjectAtIndex:ii];
        }
    }

    [tempgroup sortUsingDescriptors:[NSArray arrayWithObject:groupSorter]];
    [sortedarray addObjectsFromArray:tempgroup];

    [tempgroup release];
    tempgroup = nil;

}

In the end you'll end up with what you're looking for in sortedarray.

Halloran answered 3/2, 2010 at 9:23 Comment(1)
I think this is doing mostly the same as Benedict Cohen's response, but on your own instead of sort and predicates.Camber
H
1

You can use NSSortDescriptor. These descriptors are very useful as they let you do multiple key sort as well single key sorting. The case sensitivity and insensitivity is also easily achievable. I found a detailed example HERE

Haldeman answered 23/8, 2011 at 19:10 Comment(0)
R
0

If you have to do more complicated sorting the just "ascending" can take care of (say sort NSString as if they were floats), you might want to do something like this:

    NSDictionary *d = [self dictionaryFromURL:[NSURL URLWithString:urlStringValue]];    

    NSSortDescriptor *distanceSort = [[NSSortDescriptor alloc] initWithKey:@"distance" ascending:YES comparator:^(id left, id right) {
        float v1 = [left floatValue];
        float v2 = [right floatValue];
        if (v1 < v2)
            return NSOrderedAscending;
        else if (v1 > v2)
            return NSOrderedDescending;
        else
            return NSOrderedSame;
    }];
    NSSortDescriptor *nameSort = [NSSortDescriptor sortDescriptorWithKey:@"company_name" ascending:YES];

    NSArray *sortDescriptors = [NSArray arrayWithObjects:distanceSort, nameSort, nil];

    [distanceSort release];

    NSArray *sortedObjects = [[d allValues] sortedArrayUsingDescriptors:sortDescriptors];

    ILog();
    return sortedObjects;
Roubaix answered 16/6, 2011 at 18:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.