Nested NSArray filtering

1.1k views Asked by At

I have the need to obtain the maximum value of a property of a collection of custom objects of the same class. The objects are stored in a NSArray, and the property happens to be another NSArray of numbers.

Let me explain in detail:

NSArray *samples; // of CMData, 4000 elements

CMData is a class that models a sample, for a specific moment in time, of a set of different channels that can have different values.

@interface CMData : NSObject
@property (nonatomic) NSUInteger timeStamp;
@property (nonatomic, strong) NSArray *analogChannelData; // of NSNumber, 128 elements
@end

(I have stripped other properties of the class not relevant to the question)

So for example, sample[1970] could be:

sample.timeStamp = 970800
sample.analogChannelData = <NSArray>
    [
    [0] = @(153.27)
    [1] = @(345.35)
    [2] = @(701.02)
    ...
    [127] = @(-234.45)
    ]

Where each element [i] in the analogChannelData represents the value of that specific channel i for the timeStamp 970800

Now I want to obtain the maximum value for all the 4000 samples for channel 31. I use the following code:

NSUInteger channelIndex = 31;
NSMutableArray *values = [[NSMutableArray alloc] init]; // of NSNumber
// iterate the array of samples and for each one obtain the value for a 
// specific channel and store the value in a new array
for (CMData *sample in samples) {
    [values addObject:sample.analogChannelData[channelIndex]];
}
// the maximum 
NSNumber *maxValue = [values valueForKeyPath:@"@max.self"];

I want to replace this programming structure by a filter through an NSPredcicate or use valueForKeyPath: to obtain the maximum of the data I need.

Anyone knows how to do this without a for loop? Just using NSPredicates and/or valueForKeyPath?

Thank you very much in advance for your help.

Update 1

Finally I benckmarked the for-loop version against the keyPath version (see accepted answer) and it runs much faster so it is better to go with a for loop. Recalling some lessons from my algorithms classes, I implemented an even faster version that doesn't need an array to store the values. I just iterate over the selected channel and just choose the maximum in each iteration. This is by far the fastest version.

So:

  • version 1: for loop (see code above)
  • version 2: version with custom property (see selected answer from Marcus, update 2)
  • version 3: new code

Code for version 3:

NSUInteger channelIndex = 31;
NSNumber *maxValue = @(-INFINITY);
for (CMTData *sample in samples) {
    NSNumber *value = sample.analogChannelData[channelIndex];
    if (value) { // I allow the possibility of NSNull values in the NSArray
        if ([value compare:maxValue] == NSOrderedDescending)
            maxValue = value;
    }
}
// the maximum is in maxValue at the end of the loop

Performance:

After 20.000 iterations in iOS simulator:

  • Version 1: 12.2722 sec.
  • Version 2: 21.0149 sec.
  • Version 3: 5.6501 sec.

The decision is clear. I'll use the third version.

Update 2

After some more research, it is clear to me now that KVC does not work for infividual elements in the inner array. See the following links: KVC with NSArrays of NSArrays and Collection Accessor Patterns for To-Many Properties

Anyway because I wanted to compute the maximum of the elements it is better to iterate the array than use some tricks to make KVC work.

3

There are 3 answers

9
Marcus S. Zarra On BEST ANSWER

You can solve this with using Key Value Coding and the collection operators.

NSNumber *result = [sample valueForKeyPath:@"@max.analogDataChannel"];

Update 1

As Arcanfel mentioned, you can join the arrays together:

NSNumber *result = [samples valueForKeyPath:@"@max.@unionOfArrays.@analogChannelData"];

I would suggest reading the documentation that we both linked to. There are some very powerful features in there.

Update 2

Further to HRD's answer, he has your solution, you need to combine his changes with KVC.

Add a propert to your CMData object for currentChannel. Then you can call

[samples setValue:@(channelIndex) forKey:@"currentChannel"];

Which will set it in every instance in the array. Then call:

[samples valueForKeyPath:@"@max.analogDataForCurrentChannel"];

Then you are done.

1
Arthur Shinkevich On

I have not tested out the code yet, but I think this is exactly what you are looking for:

[samples valueForKeyPath:@"@max.(@unionOfArrays.analogChannelData)"];

I guess you can also use @distinctUnionOfArray to remove duplicate values.

Here is the link to Apple Documentation that covers collection operators.

Hope this is helpful! Cheers!

6
CRD On

A suggestion for further exploration only

Offhand it is not clear you can do this as-is with a single KVC operator. What you might consider is adding two properties to your class: currentChannel, which sets/gets the current channel; and analogChannelDataForCurrentChannel, which is equivalent to analogChannelData[currentChannel]. Then you can:

samples.currentChannel = channelIndex;
... [samples valueForKeyPath:"@max.analogChannelDataForCurrentChannel"];

with any appropriate locking between the two calls if thread-safety is required (so one thread does not set currentChannel, then a second, and then the first do the KVC operator with the second's channel...).

HTH