Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the "ALL" aggregate operation in a NSPredicate to filter a CoreData-based collection

Based on the data model below

dataModel

And based on user input I create a NSSet of managedObjects of entity Tag called selectedTags.


My problem:

[NSPredicate predicateWithFormat:@"ANY entryTags IN %@", selectedTags];

... this will return any Entry with at least one entryTag that is in the selectedTags set.

I want something along the lines of:

[NSPredicate predicateWithFormat:@"ALL entryTags IN %@", selectedTags];

... notice the only change is the "ANY" to "ALL". This illustrates what I want, but does not work.

To formulate the outcome I expect:

I'm looking for a solution that will return only Entries who's entryTags are all in the selectedTags list (but at the same time, if possible, not necessarily the other way around).

To further illustrate:

(tag)Mom
(tag)Dad
(tag)Gifts

(entry)she is a she.....(tag)mom
(entry)he is a he........(tag)dad
(entry)gifts for mom...(tags:)mom, gifts
(entry)gifts for dad.....(tags:)dad, gifts

If selectedTags contains "mom" and "gifts", then the entry "gifts for dad" will show up, since it has the tag "gifts". I'd rather have it not show :)

like image 898
Stian Høiland Avatar asked Mar 14 '11 18:03

Stian Høiland


3 Answers

I realized I could give something back here for the guidance that I have previously gotten. By using the code TechZen supplied I was able to come up with the following -- and for me highly valued -- piece of code:

- (NSArray *)unionSetOfObjectsForObjects:(NSArray *)objects {

    NSMutableSet *unionSetOfObjects = [NSMutableSet set];

    if (objects.count)
        [unionSetOfObjects unionSet:[[objects objectAtIndex:0] valueForKey:@"key"]];
       //unionSetOfObjects = [[objects objectAtIndex:0] valueForKey:@"key"];

    if (objects.count > 1)
        for (id object in objects)
            [unionSetOfObjects intersectSet:[object valueForKey:@"key"]];

    return unionSetOfObjects.allObjects;
}

If it is not immediately obvious what this code does:

It collects all the values (in my case objects) for the key key on all of the objects provided in the objects array.

This code just... tastes good, doesn't it?

like image 89
Stian Høiland Avatar answered Oct 28 '22 15:10

Stian Høiland


How about using a compound predicate? As I understand you want to return all Entries that match a list of tags not just any of them. One approach would be to create a predicate for each tag, then AND them together using a compound predicate.

NSMutableArray *predicates = [[NSMutableArray alloc] init];
for (Tag *tag in selectedTags) {
    [predicates addObject:[NSPredicate predicateWithFormat:@"ANY entryTags.tagName MATCHES %@", tag.tagName]];
}
NSPredicate *compoundPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates];

This should achieve want you want. Then just set this predicate on your request.

like image 34
Edward Huynh Avatar answered Oct 28 '22 13:10

Edward Huynh


You can't do what you want with a predicate.

The ANY and ALL operators apply to the entity being tested (in this case Entry) and not the contents of the collection (selectedTags). Either operator will return an Entry object that matches any single element of the collection. The ANY operator will return the first match it finds while the ALL operator will return all matches. In neither case will they return an entry that matches every element in the provided collection.

(It also looks like you are trying to use actual Tag objects in selectedTags. That will most likely not work either because object compares on classes without dedicated comparison methods usually fail. It is also slow. You need to compare attributes in predicates.)

Since you already have the Tag objects you want, to find the candidate related Entity objects, you just have to walk the Tag.taggedEntries relationship. Then you have to find the intersection of all the sets of Entity object to find only those Entity objects that are related to every selected Tag bject. Since there isn't an intersect collections operator, you need a loop.

if ([selectedEntries count]>=2) {
    NSMutableSet *intersectEntries=[[NSMutableSet alloc] initWithCapacity:1];
    for (int i=1; i<[selectedTags count]; i++) {
        if ([intersectEntries count]==0) {            
            [intersectEntries unionSet:[[selectedEntries objectAtIndex:(i-1)] valueForKey:@"taggedEntries"]];
        }        
        [intersectEntries intersectSet:[[selectedEntries objectAtIndex:i] valueForKey:@"taggedEntries"]];
    }
}

(Note: I didn't test this but it should work.)

Now intersectEntries should contain only those Entry objects that are related to every selected tag.

like image 29
TechZen Avatar answered Oct 28 '22 13:10

TechZen