3

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.

Community
  • 1
  • 1
Juan Catalan
  • 2,299
  • 1
  • 17
  • 23

3 Answers3

4

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.

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • The problem is that I have an NSArray of samples, not just one sample. On top of that I don't want to know the maximum for ALL the channels in one sample. I want to know the maximum for ALL the samples of ONE specific channe, lest say channel 31. – Juan Catalan Aug 23 '13 at 19:26
  • So essentially I would like to do the following: `code`NSNumber *maxValue = [samples valueForKeyPath:@"@max.analogDataChannel[channelIndex]"];`code` but I get an error. – Juan Catalan Aug 23 '13 at 19:34
  • Marcus, arcangel. Thank you very much for your help. I read all that documentation but the problem is that I can't join all the arrays because I am just interested in a SLICE of the arrays, in particular, all elements that are in a specific position, let's say index 31. I am only interested in the maximum of all the elements for index 31, not the rest. So I have to filter that. Is there a way to specify a filter in keypath to obtain the elements for index 31? `code` NSNumber *maxValue = [samples valueForKeyPath:@"@max.analogDataChannel[channelIndex]"];`code` gives an error. – Juan Catalan Aug 23 '13 at 19:45
  • Thank you Marcus!! You got the solution with the inspiration of CRD that was the one that pointed to the solution. However I still do not understand why I can't make this "Collection Accessor Patterns for To-Many Properties" to work with my data. – Juan Catalan Aug 24 '13 at 20:13
  • Because the short hand accessors for arrays and dictionaries are new and where not integrated into the KeyPath functionality. I would suggest, strongly, that you file a radar so that we can get more up votes to this issue. – Marcus S. Zarra Aug 26 '13 at 14:55
  • Sorry I am a newby in this site, what do you mean I should file a radar? I'd love to get more up votes and I have documented my question with two updates for future reference so that people with a similar problem do not have to invest time in researching this like I had to do. – Juan Catalan Aug 26 '13 at 15:16
  • Radar is Apple's bug/issue tracking system. You can find it at http://radar.apple.com. That is the ONLY way to provide feedback to Apple about their APIs and what you as a developer would like changed or improved. – Marcus S. Zarra Aug 26 '13 at 15:17
  • Ok you were referring then to more up votes on Apple website to make this feature available for future releases of Objective C, right? I will look at the radar then. – Juan Catalan Aug 26 '13 at 15:19
  • Yes, filing a radar is "voting" for a feature. The more people that file a radar on the topic the higher priority it receives. – Marcus S. Zarra Aug 26 '13 at 15:31
2

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!

2

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

CRD
  • 52,522
  • 5
  • 70
  • 86
  • Hi CRD. Thanks for your help. You seem to be the one that has understood my issue. However, samples is an NSArray not a Class. This means that if I have to implement the property you mention in CMData, then I will have to set this property in all 4000 objects of type CMData that are in the NSArray. I was thinking to use a Collection keyPath but I have not had success so far. Do you know how this works? I have followed the Apple documentation and I still get an error. Maybe I'll post another question more specific to this matter to see if anyone knows why I get an error. – Juan Catalan Aug 24 '13 at 13:24
  • @JuanCatalan - To set a property for every element in an array you use the `NSAraay` method `setValue:forKey:`. So here you can use `[samples setValue:@(channelIndex) forKey:@"currentChanel"]` - that will set the property on each sample and then the KVC line will produce the value you want. Note: if you're trying this for performance a custom iteration, as you had, may well be better. – CRD Aug 24 '13 at 18:57
  • Thanks CRD you got the solution, however maybe an iteration is just simpler. Thank you very much for your help!! – Juan Catalan Aug 24 '13 at 20:16
  • CRD: do you know if I can use "Collection Accessor Patterns for To-Many Properties"? I've been trying it but it does not work. – Juan Catalan Aug 24 '13 at 20:17
  • @JuanCatalan - The to-many patterns are for accessing all of a to-many property. In your case you are wishing to access a specific element of a to-many property, which is rather different and why nobody has suggested a working KVC-only solution to you. – CRD Aug 25 '13 at 03:04
  • you're right. It is clear to me now that KVC does not work for infivifual elements in the inner array. I've also found these links: [KVC with NSArrays of NSArrays](http://stackoverflow.com/questions/3081632/kvc-with-nsarrays-of-nsarrays?rq=1) and [Collection Accessor Patterns for To-Many Properties](http://stackoverflow.com/questions/17387553/objective-c-kvc-collection-accessor-patterns-for-to-many-properties-how-can-i) – Juan Catalan Aug 26 '13 at 13:38