I have an NSArray of custom objects and would like to filter down that array to be unique on a specific key. Most of the things I've seen while searching for an answer involve using valueForKey:, valueForKeyPath: or #distinctUnionOfObjects but those return arrays of values for that key. I want the whole object instead.
The objects are subclassed PFObjects from Parse so they are KVC compliant, and I would like them to be filtered on the objectId key.
Put this in a category on NSArray:
-(NSArray*)arrayFilteredForUniqueValuesOfKeyPath:(NSString*)keyPath
{
NSMutableSet* valueSeen = [NSMutableSet new];
return [self filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
id value = [evaluatedObject valueForKeyPath:keyPath];
if(![valueSeen containsObject:value])
{
[valueSeen addObject:value];
return true;
}
else
{
return false;
}
}]];
}
Of course, the concept is kind of flawed since you really have no way of determining which of the n objects that have any give value for the keyPath you really wanted (in this case you get the first one)
Related
Without unintentionally killing performance, does this appear at first glance to be acceptable for perhaps 200 guid strings in one list compared for equality with 100 guid strings from another list to find the matching indexes.
I have a method signature defined like so...
-(NSArray*)getItemsWithGuids:(NSArray*)guids
And I wanted to take that passed in array of guids and use it in conjunction with this array...
NSArray *allPossibleItems; // Has objects with a property named guid.
... to obtain the indexes of the items in allPossibleItems which have the matching guids from guids
My first instinct was to try indexesOfObjectsPassingTest but after putting together the block, I wondered whether the iOS framework already offers something for doing this type of compare more efficiently.
-(NSArray*)getItemsWithGuids:(NSArray*)guids
{
NSIndexSet *guidIndexes = [allPossibleItems indexesOfObjectsPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
{
SomeObjWithGuidProperty *someObject = obj;
for (NSString *guid in guids) {
if ([someObject.guid isEqualToString:guid]) {
return YES;
}
}
return NO;
}];
if (guidIndexes) {
// Have more fun here.
}
}
Since you're working with Objective-C (not Swift) check out YoloKit. In your case, you can do something like:
guids.find(^(NSString *guid){
return [someObject.guid isEqualToString:guid];
});
My thought would be to use a set -
-(NSArray*)getItemsWithGuids:(NSArray*)guids inAllObjects:(NSArray *)allObjects
{
NSSet *matchGuids=[NSSet setWithArray:guids];
NSMutableArray *matchingObjects=[NSMutableArray new];
for (SOmeObjectWithGuidProperty *someObject in allObjects) {
if ([matchGuids contains:someObject.guid]) {
[matchingObjects addObject:someObject];
}
}
return [matchingObjects copy];
}
Your code looks like it would have O(n^2) performance, which is bad. I think the solution of converting guids to an NSSet and then using NSSet's containsObject would likely be much more performant. You could rewrite your indexesOfObjectsPassingTest code to use an NSSet and containsObject pretty easily.
If order doesn't matter much, I would suggest to change data structure here. Instead of using NSArray, consider to use NSDictionary with guid as key and someObject as value. In this case, you should use -[NSDictionary objectsForKeys:notFoundMarker:] method to obtain objects.
It will work much faster, than enumeration trough 2 arrays. If the NSDictionary key have a good hash function, accessing an element, setting an element, and removing an element all take constant time. NSString has good hash.
-(NSArray*)getItemsWithGuids:(NSArray*)guids {
NSArray *objectsAndNulls = [allPossibleItemsDictionary objectsForKeys:guids notFoundMarker:[NSNull null]];
if (objectsAndNulls) {
// Have more fun here.
// You should check that object in objectsAndNulls is not NSNull before using it
}
return objectsAndNulls;
}
UPD Unfortunately, there is no way to pass nil as notFoundMarker. If you can't provide usable notFoundMarker value and don't want to perform additional checks, you can query objects one by one and fill NSMutableArray. In this case you will avoid pass trough array to remove NSNulls:
-(NSArray*)getItemsWithGuids:(NSArray*)guids {
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:guids.count];
for (NSString *guid in guids) {
SomeObjWithGuidProperty *object = allPossibleItemsDictionary[guid];
if (nil != object) {
[objects addObject:object];
}
}
if (nil != objects) {
// Have more fun here.
}
return object;
}
I have created simple XCTest to test distinctUnionOfObjects. All the test cases are passing except one which is isKindOfClass (Last XCTAssertTrue). Any idea why it's changing the class when you do distinctUnionOfObjects.
- (void)testUsersPredicate
{
NSArray *usersBeforePredicate = [[self userData] users];
XCTAssertEqual([usersBeforePredicate count] , 34u, #"We need 34");
XCTAssertTrue([[usersBeforePredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], #"Object is not ICEUsersModelObject class");
NSString *distinctUsersKeyPath = [NSString stringWithFormat:#"#distinctUnionOfObjects.%#", #"userName"];
NSArray* usersAfterPredicate = [usersBeforePredicate valueForKeyPath:distinctUsersKeyPath];
XCTAssertEqual([usersAfterPredicate count] , 30u, #"We need 30");
XCTAssertTrue([[usersAfterPredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], #"Object is not ICEUsersModelObject class");
}
As the right key path on your distinctUnionOfObjects is userName, the -valueForKeyPath: call will return an NSArray of distinct userNames (not user objects).
From Apple's KVC Programming Guide:
The #distinctUnionOfObjects operator returns an array containing the distinct objects in the property specified by the key path to the right of the operator.
Change the last test case to check for [NSString class] and it should pass.
Alternatives
Using equality:
If the userNameproperty is supposed to serve as a unique identifier, you could enforce that by overriding -isEqual: and -hashon the user object to reflect this:
- (BOOL)isEqual:(id)object {
return ([object isKindOfClass:self.class] && [object.userName isEqual:self.userName]);
}
- (NSUInteger)hash {
return self.userName.hash;
}
This can benefit your overall model design and opens up a lot of additional options, like this one that obtains a collection of distinct users reg. userName in one line - NSSet is very fast when used for this:
NSArray *uniqueUsers = [[NSSet setWithArray:users] allObjects];
Note: I re-used the hashing function of NSString for the user hash, which has a subtle pitfall; -[NSString hash] only guarantees uniqueness for strings of up to 96 characters! This is not in the docs and took me almost a day to track down in production code. (see Apple's implementation of CFString.c - search for __CFStrHashCharacters)
Using NSPredicate:
Here's a, let's say, 'creative' solution that uses a predicate. However, some kind of iteration is needed, because the predicate condition would otherwise have to be a function of its own result:
NSMutableArray *__uniqueUsers = [NSMutableArray array];
[[users valueForKeyPath:#"#distinctUnionOfObjects.userName"] enumerateObjectsUsingBlock:^(id name, NSUInteger idx, BOOL *stop) {
NSArray *uniqueUser = [users filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id user, NSDictionary *bindings) {
return [user.userName isEqual:name];
}]];
if (uniqueUser.count > 0)
[__uniqueUsers addObject:uniqueUser.lastObject];
}];
NSArray *uniqueUsers = [NSArray arrayWithArray:__uniqueUsers];
It obtains a collection of unique userNames, iterates over it, selects exactly one user for each name and adds that to the output array.
You test has no sense. You can't use distinct in this way because as #mvanellen told you, you should change the class in NSString due the fact that you are searching for distinct username.
You instead are trying to get the list of objects considering distinct username, but it is conceptually wrong. You should try to get the list of the username if you want, but not of the entire objects.
Consider to have in your array:
ITEM 1: aav / otherValue1
ITEM 2: aav / otherValue2
ITEM 3: matteo / otherValue3
for sure the function would return ITEM 3, but being a distinct query, which from ITEM 1 and ITEM 2 should it takes?
Think about this ;)
I have two NSMutableArrays, filled with dictionary objects, like
NSMutableArray * bigArray = #[dictionary1,dictionary2,dictionary3];
NSMutableArray * smallArray = #[dictionary1];
I want to remove the common elements (dictionay1) from the big array
Note: small array is subset of big array and elements are distinct
[bigArray removeObjectsInArray:smallArray];
removes all objects in bigArray that are contained in smallArray.
If each element is distinct (as you say in the comments) then you can filter the array using a predicate that removes all the objects that inside of the smaller subset. Since you are using an array in your question, I assume that the order of the objects matter.
If you need to preserve the order, then you will need to filter the array. Converting to a set will break the order (unless you are able to resort it afterwards.
You are using mutable arrays in your question, so I will do the same. Just be aware that the original array is actually modified:
NSMutableArray *original = [NSMutableArray arrayWithArray:#[#"A", #"E", #"C", #"B", #"D"]];
NSMutableArray *subset = [NSMutableArray arrayWithArray:#[#"C", #"B"]];
NSLog(#"before : %#", original);
[original filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
// return YES for objects to keep
return ![subset containsObject:evaluatedObject];
}]];
NSLog(#"after : %#", original);
The output of this code is:
before : (
A,
E,
C,
B,
D
)
after : (
A,
E,
D
)
You can see that the order is preserved.
If you don't want to modify the original array you can produce a new filtered array instead. The small difference is that you call filteredArrayUsingPredicate: instead of filterUsingPredicate:. The predicate is the same:
NSArray *filtered = [original filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
// return YES for objects to keep
return ![subset containsObject:evaluatedObject];
}]];
NSSet is perfect class to do this kind of job. Try that;
NSMutableSet *bigSet = [NSMutableSet setWithArray:bigArray];
[bigSet minusSet:[NSMutableSet setWithArray:smallArray]];
bigArray = [[bigSet allObjects] mutableCopy];
The thing worth to point is that if your array will store duplicate and you convert it to NSSet the duplicate will be removed in that case you shouldn't use it. The second thing is it can change order of the elements in your array.
What I am trying to achieve is to search an array for a string, here is the code for searching the array
if ([EssentialsArray containsObject:imageFilePath]) {
NSLog(#"YES");
}
else {
NSLog(#"NO");
NSLog(#"%#", ImageFilePath);
NSLog(#"%#", EssentialsArray);
}
The NSLogs return this for the imageFilePath:
/var/mobile/Applications/5051DC84-CAC8-4C1D-841D-5539A1E28CB1/Documents/12242012_11025125_image.jpg
And this for the EssentialsArray:
(
{
EssentialImage = "/var/mobile/Applications/5051DC84-CAC8-4C1D-841D-5539A1E28CB1/Documents/12242012_11025125_image.jpg";
}
)
And then obviously they return a "NO" value because the array couldn't find the string.
Thanks in advance
The issue is that the invocation of [EssentialsArray containsObject:imageFilePath] clearly assumes that EssentialsArray is an array of strings, whereas it's not. It's an array of dictionary entries with one key, EssentialImage.
There are at least two solutions. The first is to make an array of strings for those dictionary entries whose key is EssentialImage:
NSArray *essentialImages = [essentialsArray valueForKey:#"EssentialImage"];
if ([essentialImages containsObject:imagePath])
NSLog(#"YES");
else
NSLog(#"NO");
Depending upon the size of your essentialsArray (please note, convention dictates that variables always start with a lower case letter), this seems a little wasteful to create an array just so you can do containsObject, but it works.
Second, and better in my opinion is to use fast enumeration to go through your array of dictionary entries, looking for a match. To do this, define a method:
- (BOOL)arrayOfDictionaries:(NSArray *)array
hasDictionaryWithKey:(id)key
andStringValue:(NSString *)value
{
for (NSDictionary *dictionary in array) {
if ([dictionary[key] isEqualToString:value]) {
return YES;
}
}
return NO;
}
You can now check to see if your array of dictionaries has a key called #"EssentialImage" with a string value equal to the string contained by imagePath
if ([self arrayOfDictionaries:essentialsArray
hasDictionaryWithKey:#"EssentialImage"
andStringValue:imagePath])
NSLog(#"YES");
else
NSLog(#"NO");
Update:
You can also use predicates:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"EssentialImage contains %#", imagePath];
BOOL found = [predicate evaluateWithObject:array];
I have a custom class extending NSObject. I am maintaining NSMutableArray of this class objects. Here is the situation,
customObject-class {
NSString *name;
int ID;
.....and many other properties;
}
customObjectsArray [
customObject1,
customObject2,
...etc
]
Now I am trying to use filterUsingPredicate to remove objects that has nil names, like below but it returns very few or none objects while I know that there are hundreds of objects that has name not nil or empty. Could someone please tell me what could be wrong here.
[customObjectsArray filterUsingPredicate:[NSPredicate predicateWithFormat:#"name != nil"]];
Why won't you try like this:
NSMutableArray *array=...;
[array filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
CustomObject *customObject=(CustomObject *) evaluatedObject;
return (customObject.name!=nil);
}]];
As I replied to #rdelmar, I found an issue. This predicate was getting called before customObject1's data were actually initialised. I should check the status of data flag that says data has been initialised for this particular object and then apply filter. It worked. If data is not initialised all object's name is off course nil!