Abusing NSPredicate

NSPredicate is a nifty class. It’s intended use is to be an objected-oriented way of expressing a truth statement. We see this being used with things like -[NSArray filteredArrayUsingPredicate:], -[NSFetchRequest setPredicate:], and more. NSPredicate, however, can offer us a lot more.

There are 2 things that make NSPredicate so insanely awesome:

  1. Its string parser
  2. The evaluation power of NSExpression (one of NSPredicate's support classes)

NSPredicate Parsing

One of the best things about NSPredicate is its parsing engine. Given a well-formatted NSString, NSPredicate will turn it into a tree of NSCompoundPredicate and NSComparisonPredicate objects. This can be used to our advantage. If we have a string that represent some boolean expression, we can tap NSPredicate to parse it for us and give us back an organized syntax tree.

For this example, let’s consider the following:

(a | b) & c | (b | d)

If we just try to run this through the predicate parser, we’ll get an exception, for 2 reasons:

  1. A predicate is a truth statement. This expression is simply a value. It is not being compared to anything, and as such does not represent a true or false statement, but simply a value.
  2. Each sub-expression of a predicate must itself be a predicate. Since “c" (for example) is not being compared to anything, it is not a valid statement.

Fixing the first problem is easy. We can just tack “= 0" onto the end of the statement:

(a | b) & c | (b | d) = 0

As for fixing the second, we can do that with a little bit of find-and-replace by regular expression. For simplicity, we’ll say that each one of these identifiers (a, b, c, and d) must be alphanumeric. We can then use something like NSRegularExpression (on iOS) or RegexKit (on Mac) to replace “([a-z0-9]+)" with "\1 = 0”, thereby giving us:

(a = 0 | b = 0) & c = 0 | (b = 0 | d = 0) = 0

We can now run this through +[NSPredicate predicateWithFormat:] to get our parsed tree. From there you can recursively walk the tree and manipulate things as much as you like.

NSExpression Solving

Let’s say you have a string that contains some sort of mathematical statement. Perhaps it’s user-entered, or perhaps it’s dynamically generated. For our purposes, we’re going to use:

22/7.0 + (13*42) - 1024

Our calculator shows us that we should be getting -474.857142857142857 as the result.

But how can we evaluate this dynamically? Well, you could use something like Graham Cox’s excellent GCMathParser to parse and evaluate it, but this has a limitation: it’s a bit difficult (read: nearly impossible) to extend to use functions not supported by the parser.

Fortunately, we can abuse NSPredicate to do the work for us.

NSPredicate has a wonderful parsing engine (which we’ve already abused), and it builds a tree built out of NSExpression objects, which have this wonderful method called -expressionValueWithObject:context: which will evaluate everything for us. Unfortunately, there’s no way to build an NSExpression tree out of an arbitrary NSString. However, NSPredicate can.

So we just need to make out statement into a predicate, run it through the parser, and extract the appropriate value! This is really simple. To make something into a predicate, we simply need to add an operator and a comparison value:

22/7.0 + (13*42) - 1024 = 0

Now, we can pump it through NSPredicate:

NSPredicate * parsed = [NSPredicate predicateWithFormat:@"22/7.0 + (13*42) - 1024 = 0"];

We know that this is an NSComparisonPredicate (since it’s of the form <expression> <operator> <expression>), so we can simply extract the leftExpression:

NSExpression * left = [(NSComparisonPredicate *)parsed leftExpression];

And finally, we can now evaluate it:

NSNumber * result = [left expressionValueWithObject:nil context:nil];
NSLog(@"result: %@", result);

Wonderfully, this logs -474.8571428571429, which is exactly what we were hoping for.

  1. lewesde reblogged this from funwithobjc
  2. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y1Sc18N
blog comments powered by Disqus