Sunday, March 13, 2011

NSPredicate: problem solved (almost)

It only took me about four hours, but with big help from this post, I solved the problem of filtering a string containing an array of (possibly invalid) filepaths. And, in the meantime, Dave DeLong himself had responded to my question on StackOverflow from yesterday, which if I'd just waited a bit more, would have saved me some time. Thanks, Dave.

As I mentioned (here), we're just wrapping a filtering routine up in a category on NSString. Here is the first part of the code file including the category:

#import <Foundation/Foundation.h>

@interface NSString (ValidatingFilepathArray)
- (NSNumber *) validate;

@implementation NSString (ValidatingFilepathArray)

- (NSNumber *) validate {
NSArray *A = [self componentsSeparatedByString:@":"];
NSString *s, *p;
NSFileManager *fm = [NSFileManager defaultManager];
for (p in A) {
s = [p stringByExpandingTildeInPath];
if ([fm fileExistsAtPath:s]) {
NSLog(@"passed: %@", p);
else {
NSLog(@"failed: %@", p);
return [NSNumber numberWithBool:NO];
return [NSNumber numberWithBool:YES];

In the code at the bottom of the post, we construct a "function expression" from the new NSString method. Two things really confused me. First, in the method


the "function" is the object itself. And the second thing was figuring out the format string for the predicate that wraps up our expression:

@"FUNCTION(SELF, 'validate') isEqual:YES"

This is the output:

> gcc -o test test.m -framework Foundation
> ./test
2011-03-13 16:53:42.974 test[3494:903] passed: ~
2011-03-13 16:53:42.976 test[3494:903] passed: ~/Desktop
2011-03-13 16:53:42.977 test[3494:903] expression for ~:~/Desktop: YES
2011-03-13 16:53:42.978 test[3494:903] failed: xyz
2011-03-13 16:53:42.978 test[3494:903] expression for xyz: NO
2011-03-13 16:53:42.979 test[3494:903] passed: ~
2011-03-13 16:53:42.980 test[3494:903] passed: ~/Desktop
2011-03-13 16:53:42.980 test[3494:903] failed: xyz
2011-03-13 16:53:42.981 test[3494:903] filtered: ~:~/Desktop
2011-03-13 16:53:42.981 test[3494:903] passed: ~/Desktop
2011-03-13 16:53:42.982 test[3494:903] evaluate: YES

First we construct the expression and evaluate it. In the second part, we construct the predicate and use it to filter an array or just "evaluate" a string. Still to do: test it in an app with bindings and an NSTextField or NSTableView.


int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString *validPathArray = @"~:~/Desktop";
NSExpression *f = [NSExpression expressionForConstantValue:validPathArray];
NSExpression *e = [NSExpression expressionForFunction:f
selectorName:@"validate" arguments:nil];
NSNumber *result = [e expressionValueWithObject:nil context:nil];
NSArray *responses = [NSArray arrayWithObjects:@"NO",@"YES",nil];
NSLog(@"expression for %@: %@", validPathArray,
[responses objectAtIndex:[result intValue]]);

NSString *invalidPathArray = @"xyz";
f = [NSExpression expressionForConstantValue:invalidPathArray];
e = [NSExpression expressionForFunction:f
selectorName:@"validate" arguments:nil];
result = [e expressionValueWithObject:nil context:nil];
NSLog(@"expression for %@: %@", invalidPathArray,
[responses objectAtIndex:[result intValue]]);


NSPredicate *p;
p = [NSPredicate predicateWithFormat:@"FUNCTION(SELF, 'validate') isEqual:YES"];
NSArray *A = [NSArray arrayWithObjects: validPathArray, invalidPathArray, nil];
NSArray *fA = [A filteredArrayUsingPredicate:p];
for (id obj in fA) {
NSLog(@"filtered: %@", obj);
BOOL yesorno = [p evaluateWithObject:@"~/Desktop"];
NSLog(@"evaluate: %@", [responses objectAtIndex:(int) yesorno]);
[pool drain];
return 0;