7

I am trying to filter a mutable array of objects using NSPredicate and am having trouble access the level that contains the property I would like to filter on.

To give a simplified example consisting of the similar custom objects.

  • Grandparent
  • Parent
  • Child

I have an NSMutableArray of Grandparents and I would like to find all the Grandparent Objects that have GrandChildren of age 10. Therefore the grandchildren are two levels deep from the root. Child has an age property amongst other things.

ie. Grandparents has an array property Parents and Parents has an array property Children and Children has an integer property age.

The following NSPredicate has returned no results. "SELF.parents.children.age == 10".

I realise that as these are nested collections this predicate is likely the wrong way to go about it but I am stuck as to how to access that level. Perhaps via a Subquery or Collection Operator but I cannot work it out.

One thing to keep in mind is that I obviously still want GrandParents that have multiple Grandchildren of different ages, one of which is aged 10.

Daniel Bowden
  • 988
  • 1
  • 10
  • 23

2 Answers2

16

The "obvious" solution would be the predicate:

"ANY parents.children.age == 10"

However, the "ANY" operator does not work with nested to-many relationships. Therefore, you need a SUBQUERY:

NSArray *grandParents = your array of GrandParent objects;
NSPredicate *predicate = [NSPredicate
   predicateWithFormat:@"SUBQUERY(parents, $p, ANY $p.children.age == 10).@count > 0"];
NSArray *filtered = [grandParents filteredArrayUsingPredicate:predicate];

Remarks:

  • Using SELF in the predicate is not necessary. filteredArrayUsingPredicate applies the predicate to each GrandParent object in the array.
  • The usage of SUBQUERY in predicates seems to be poorly documented. There is one example in the NSExpression class reference. See also Quick Explanation of SUBQUERY in NSPredicate Expression.
  • In this case, the predicate inside SUBQUERY is applied to each Parent of a single GrandParent. The SUBQUERY returns the parents that have any child aged 10. So "SUBQUERY(...).@count > 0" evaluates to TRUE if the grandparent has at least one parent that has any child aged 10.

ADDED: I just found out that it can actually be done without SUBQUERY:

NSPredicate *predicate = [NSPredicate
    predicateWithFormat:@"ANY parents.@unionOfArrays.children.age == 10"];

works and gives the desired result. (It could be less effective than the SUBQUERY, but I did not test that.)

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Interesting solution. I'm just trying to get my head around the syntax. Given that I start with the Array of grandparents and have to get two levels deep would I have to start with SELF.parents? Otherwise how would the subquery know about parents? If possible are you please able to wrap it around some ObjC NSPredicate example? – Daniel Bowden Jun 01 '13 at 02:25
  • @DanielBowden: I will expand the answer later when I am at my computer. – Martin R Jun 01 '13 at 03:51
  • Also is this missing an extra ( at the start? Why is the count>0 necessary, isn't that implied with a query? – Daniel Bowden Jun 01 '13 at 07:59
  • @DanielBowden: Yes, there was a syntax error in the predicate. I have fixed that and added explicit (tested!) code and some explanations. - I hope that helps, otherwise let me know if you need more information. – Martin R Jun 01 '13 at 08:13
12

As an alternative to Martin R's answer, you might consider using a block predicate instead. Something like this:

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * bindings) {
  GrandParent *grandparent = evaluatedObject;
  for (Parent *parent in grandparent.parents)
    for (Child *child in parent.children)
      if (child.age == 10)
        return YES
  return NO;
}];

Assuming GrandParent, Parent and Child are the appropriate class names of the various objects.

Personally I prefer this form, because I always feel with a string predicate that I'm mixing languages in the code, which I think makes it less readable. The choice is obviously up to you though.

Update: Having re-read the question, I now realise that the condition was more complex than I originally thought. I've updated my answer to loop over the parents and children, but Martin R's answer is now clearly a lot simpler. Still this is a possible solution to consider.

James Holderness
  • 22,721
  • 2
  • 40
  • 52
  • Yes, you are right! - (Even if the question was about an array, I had Core Data in my mind when giving the answer, and block-based predicates do not work with Core Data fetch requests.) – Martin R May 31 '13 at 13:49
  • I agree, this is clear. But this format doesn't just work with array of dictionaries. But the formatString works thanks to KVC. In this case we got to [evaluatedObject valueForKeyPath:@"parents"] etc. – Vignesh May 31 '13 at 14:26
  • @Vignesh I didn't see any mention of dictionaries by the OP, but you're correct that the code wouldn't work as given without at least a cast (which I've now added to the answer). Having re-read the question, though, even that is not enough, given that the parents and children properties are arrays. – James Holderness May 31 '13 at 14:51
  • Thanks for updating the answer. The thing I said about dictionaries is not related to this question. – Vignesh May 31 '13 at 15:23
  • Oh I see - you just mean as a general solution. That's definitely another point in favour of string predicates. I'm still partial to using blocks though. :) – James Holderness May 31 '13 at 15:31
  • Thanks James this is more readable and easier to understand though as I possibly will need to set up multiple of these I may go with the shorter version. Much appreciated anyway. – Daniel Bowden Jun 01 '13 at 09:38
  • This pointed me in the right direction and ended up being the more readable approach after some refactoring on my side it. The block predicate is very helpful. – Daniel Bowden Jun 04 '13 at 13:33
  • you can not use this with Core data's default store. – RK- Dec 18 '14 at 04:53