In a recent post I covered [how to make a simple `NSPredicateEditorRowTemplate`](http://tumblr.com/xqor81lfy). In this post we're going to go a bit deeper. ## The Setup In our last example, we did some simple date comparisons, where we were comparing an `NSTimeInterval` against a property that represented a duration. For example, if we had an array of `Song` objects, then we could use that row template to construct a predicate to find all songs that were longer than 5 minutes (`duration > 300`). But what if our objects don't have a duration property? What if they have a date property, such as `the date this song was added`? Well, we can use the built-in initializers of `NSPredicateEditorRowTemplate` to create a template where the right expression type is `NSDateAttributeType`. This is all well and good, but it has a fundamental flaw: It can only compare against static dates. In other words, I can easily create a row template for predicates of the form ` `. But what if I want to do something like ` `? Is there a way to do relative date comparison? The answer is "yes", but this is going to take a lot of explanation. So buckle up, and let's dive right in! ## The Problems There are two major problems to overcome when doing relative date comparison and integrating that with `NSPredicateEditorRowTemplate`: 1. `NSPredicateEditor` compacts left expressions into a unique list. Where a left expression supports multiple operators, those are also uniqued. In other words, you can't do both `myKeyPath = ` and `myKeyPath = ` in the same `NSPredicateEditor`. It doesn't matter if these are defined in different row templates. `NSPredicateEditor` will still find both of them, and won't know what to display when the user chooses `myKeyPath` from the first popup and `=` from the second popup. What should it display? A textfield? A popupmenu? We care about this because if we want to support both `myDate ` and `myDate `, these both boil down to the same thing (after doing dynamic date resolution): find if one date is after another date. In other words, the same left expression, the same operator, but different right expressions. `NSPredicateEditor` doesn't support this. We need to find a way around this. 2. `NSPredicate` doesn't have any obvious date addition or date subtraction features. We need to figure out how to add and subtract arbitrary time intervals from arbitrary dates. ## The Solutions While these may seem like road-blocker problems, they're really not too bad. ### Solving the operator problem The key to tricking `NSPredicateEditor` into showing different right expressions for two operators that are fundamentally the same is to not use the same operator. These are the operators that `NSPredicateEditor` recognizes: typedef enum { NSLessThanPredicateOperatorType = 0, NSLessThanOrEqualToPredicateOperatorType, NSGreaterThanPredicateOperatorType, NSGreaterThanOrEqualToPredicateOperatorType, NSEqualToPredicateOperatorType, NSNotEqualToPredicateOperatorType, NSMatchesPredicateOperatorType, NSLikePredicateOperatorType, NSBeginsWithPredicateOperatorType, NSEndsWithPredicateOperatorType, NSInPredicateOperatorType, NSCustomSelectorPredicateOperatorType, NSContainsPredicateOperatorType, NSBetweenPredicateOperatorType } NSPredicateOperatorType; You'll notice that only six of 14 operators are for numeric comparisons (`<`, `<=`, `>`, `>=`, `=`, `!=`). The rest are for string comparisons. This means we have 7 or 8 operators that are sitting around unused when we're doing a numeric comparison (and date comparisons are really numeric comparisons). The other key to tricking `NSPredicateEditor` into showing different right expressions is to realize that `NSPredicateEditor` is just a UI. It doesn't care what the actual underlying `NSPredicate` is. It's just going to show whatever it's given, *even if that is different from the actual `NSPredicate`*. So the solution: #define NSInTheLastPredicateOperatorType NSMatchesPredicateOperatorType #define NSInTheNextPredicateOperatorType NSLikePredicateOperatorType That's it. Any time we want to show an "in the last"-style right expression, we're going to tell the editor "this is the 'matches'" operator. Then we're just going to do a bit of handling in code to not actually use the `matches` operator, but the proper comparator instead. Simple! ### Solving the relative date problem Have you ever looked at how dates are represented in predicates? It's quite interesting: NSDate * d = [NSDate date]; NSLog(@"%@", [NSPredicate predicateWithFormat:@"%@ != 0", d]); //logs "CAST(312333057.230785, "NSDate") != 0" Well now, *that's* curious. We're taking some number and "casting" it as an `NSDate`. But what's that number? Let's do: NSLog(@"%f", [d timeIntervalSinceReferenceDate]); //logs "312333057.230785" So this number is a time interval! This is very convenient. But what about all this casting? If we capture the left expression of this predicate, we will find that it's an `NSExpression` of type `NSFunctionExpressionType`, and that the `-function` of this expression is `@"castObject:toType:"`. Further introspection reveals that the `-arguments` to this function are two `NSExpressions`: The first is a constant value expression holding an `NSNumber`, whose `-doubleValue` is `312333057.230785`, and the second is a constant value expression holding the string `@"NSDate"`. This is very important, because we learn 3 major things: 1. There is an undocumented function for `NSExpression`: `castObject:toType:`. 2. This function wants a number as its first argument 3. This function wants a string (that represents a class) as its second argument. Armed with this information, we can surmise: 1. We can create predicates that have the string `CAST()` in them. We don't have to rely on `predicateWithFormat:` to insert that for us. 2. Any number, or any function that results in a number, can be used as the first parameter to a `CAST()` function and be cast to an `NSDate`. That number represents a time interval since the first instant of `1 January 2001, GMT` (the reference date). 3. We can cast objects to other kinds of classes Let's test each one of these things: NSPredicate * p = [NSPredicate predicateWithFormat:@"%@ > CAST(0, 'NSDate')", [NSDate date]]; NSLog(@"%d", [p evaluateWithObject:nil]); //logs "1". right now is after our reference date! p = [NSPredicate predicateWithFormat:@"%@ > CAST(1+2+3+4+5+6, 'NSDate')", [NSDate date]]; NSLog(@"%d", [p evaluateWithObject:nil]); //logs "1". right now is after 21 seconds after the reference date! p = [NSPredicate predicateWithFormat:@"CAST(%@, 'NSNumber') > 0", [NSDate date]]; NSLog(@"%d", [p evaluateWithObject:nil]); //logs "1". We can cast dates to their numerical equivalent! So all three of our assumptions appear to be correct! The last one is the most interesting, however. If we pull out the `leftExpression` of the predicate and evaluate it, we get `312333057.230785`. In other words, casting a date to a number simply converts it to its reference date time interval! Since we could cast a number to a date, it makes perfect sense that we can cast a date to a number. The last trick is "how do we get the current date without having to substitute it in every time?". Fortunately, there's a very simple answer: `now()`. `now()` is one of the built-in functions supported by `NSExpression`, and it (conveniently enough) returns an `NSDate` representing the current date and time. Now that we know this, we can construct relative date predicates! So for example, let's do "someDate is in the last 30 days". If a date is in the last 30 days, that means that `someDate` is after `now - 30 days`: [NSPrediate predicateWithFormat:@"someDate > CAST(CAST(now(), 'NSNumber') - %d, 'NSDate')", (30*86400)]; Working from the inside-out, we have: - `now()` - retrieve the current `NSDate` - `CAST(now(), 'NSNumber')` - convert the current date into an `NSNumber` - `CAST(now(), 'NSNumber') - %d` - subtract a time interval from the current timestamp. This difference must be in seconds. `30*86400` is 30 days in seconds (there are 86,400 seconds in a day) - `CAST(CAST(now(), 'NSNumber') - %d, 'NSDate')` - once we've done our subtraction, cast the time interval back to an `NSDate` At this point, we can compare it against our target date. We could've done the date subtraction on the value returned by the left expression, but there are some circumstances where that's not a good idea (primarily when integrating with CoreData), so we'll keep it in the right. Voilà! Date subtraction in an `NSPredicate`! Doing something like "` is during `" would be slightly more complex (it would involve a `BETWEEN` comparison [or something functionally identical]), but still do-able. Even more fun would be a predicate of the form "` is during the of `". Also very difficult, but again not impossible. Maybe we'll do those in another post. Anyway... ## The Row Template Our row template is going to be very similar to the template in my earlier post, because (again) we're doing `NSTimeInterval` manipulations. Let's get started: ### The Interface @interface DDRelativeDateRowTemplate : NSPredicateEditorRowTemplate { NSPopUpButton * unitPopUpButton; } - (id) initWithLeftExpressions:(NSArray *)leftExpressions; @end Nothing new to see here. ### The Implementation #define NSInTheLastPredicateOperatorType NSMatchesPredicateOperatorType #define NSInTheNextPredicateOperatorType NSLikePredicateOperatorType Some `#defines` to make things a bit more readable in our code @implementation DDRelativeDateRowTemplate - (id) initWithLeftExpressions:(NSArray *)leftExpressions { NSAttributeType rightType = NSDoubleAttributeType; //NSTimeInterval is a typedef'd double NSComparisonPredicateModifier modifier = NSDirectPredicateModifier; //don't need "ANY" or "ALL" NSArray * operators = [NSArray arrayWithObjects: [NSNumber numberWithUnsignedInteger:NSInTheLastPredicateOperatorType], [NSNumber numberWithUnsignedInteger:NSInTheNextPredicateOperatorType], nil]; NSUInteger options = 0; return [super initWithLeftExpressions:leftExpressions rightExpressionAttributeType:rightType modifier:modifier operators:operators options:options]; } Our `init` method is almost identical to the `init` method in our previous template, with the exception that this time we're only supporting 2 operators. Notice that we're telling the predicate editor that these are the "matches" and "like" operators. Again, this is only to get around problem #1 (see above). static NSString * unitNames[] = {@"seconds", @"minutes", @"hours", @"days", @"weeks"}; static NSInteger unitIntervals[] = {1, 60, 3600, 86400, 604800}; #define numberOfUnits() (sizeof(unitNames)/sizeof(unitNames[0])) - (NSPopUpButton *) unitPopUpButton { if (unitPopUpButton == nil) { unitPopUpButton = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; NSMenu * unitMenu = [unitPopUpButton menu]; for (int i = 0; i < numberOfUnits(); ++i) { [unitMenu addItemWithTitle:unitNames[i] action:NULL keyEquivalent:@""]; } } return unitPopUpButton; } - (void) dealloc { [unitPopUpButton release]; [super dealloc]; } - (void) getBase:(NSTimeInterval *)base unit:(NSInteger *)unit fromTimeInterval:(NSTimeInterval)timeInterval { if (base == nil || unit == nil) { [NSException raise:NSInvalidArgumentException format:@"base and unit cannot be nil"]; } for (NSInteger unitIndex = numberOfUnits() - 1; unitIndex >= 0; unitIndex--) { if (timeInterval >= unitIntervals[unitIndex]) { *base = timeInterval / unitIntervals[unitIndex]; *unit = unitIndex; return; } } } - (NSTimeInterval) timeIntervalFromBase:(NSTimeInterval)base unit:(NSInteger)unitIndex { if (unitIndex >= numberOfUnits()) { [NSException raise:NSInvalidArgumentException format:@"unitIndex beyond max allowed bounds (%d)", numberOfUnits()]; return 0; } return (base * unitIntervals[unitIndex]); } This code is identical to the code that was in our previous template, so I won't explain it here. - (NSArray *) templateViews { NSMutableArray * views = [[super templateViews] mutableCopy]; [views addObject:[self unitPopUpButton]]; NSPopUpButton * operatorView = [views objectAtIndex:1]; [[operatorView itemAtIndex:0] setTitle:@"is in the last"]; [[operatorView itemAtIndex:1] setTitle:@"is in the next"]; return [views autorelease]; } Our `-templateViews` method is almost the same, except that we're altering the title of of the operators. If we were to leave out that bit, then we would see "matches" and "is like" in the interface. By changing the title here, we're providing a new default value for the operator title. Note that this can be changed via localization, and more importantly, your `NSLocalizedString` macros for generating the strings file *should use the values listed here, **not** the "matches" and "is like" values*. Also note that operators show up in the interface in the same order in which they're given in the `-init` method. - (double) matchForPredicate:(NSPredicate *)predicate { if (![predicate isKindOfClass:[NSComparisonPredicate class]]) { goto errorExit; } NSComparisonPredicate * comparison = (NSComparisonPredicate *)predicate; NSExpression * left = [comparison leftExpression]; if (![[self leftExpressions] containsObject:left]) { goto errorExit; } NSPredicateOperatorType operator = [comparison predicateOperatorType]; if (operator != NSGreaterThanPredicateOperatorType && operator != NSLessThanPredicateOperatorType) { goto errorExit; } NSExpression * right = [comparison rightExpression]; if ([right expressionType] != NSFunctionExpressionType) { goto errorExit; } if (![[right function] isEqual:@"castObject:toType:"]) { goto errorExit; } NSArray * outerCastArguments = [right arguments]; //we can pull these out without bounds checking because a CAST() function *must* have 2 arguments NSExpression * firstArgument = [outerCastArguments objectAtIndex:0]; { //the first argument must be either an addition or subtraction function if ([firstArgument expressionType] != NSFunctionExpressionType) { goto errorExit; } if (![[firstArgument function] isEqual:@"add:to:"] && ![[firstArgument function] isEqual:@"from:subtract:"]) { goto errorExit; } NSArray * relativeArguments = [firstArgument arguments]; NSExpression * firstRelativeArgument = [relativeArguments objectAtIndex:0]; { if ([firstRelativeArgument expressionType] != NSFunctionExpressionType) { goto errorExit; } if (![[firstRelativeArgument function] isEqual:@"castObject:toType:"]) { goto errorExit; } NSArray * innerCastArguments = [firstRelativeArgument arguments]; NSExpression * firstInnerArgument = [innerCastArguments objectAtIndex:0]; if ([firstInnerArgument expressionType] != NSFunctionExpressionType) { goto errorExit; } if (![[firstInnerArgument function] isEqual:@"now"]) { goto errorExit; } NSExpression * secondInnerArgument = [innerCastArguments objectAtIndex:1]; if ([secondInnerArgument expressionType] != NSConstantValueExpressionType) { goto errorExit; } if (![[secondInnerArgument constantValue] isEqual:@"NSNumber"]) { goto errorExit; } } NSExpression * secondRelativeArgument = [relativeArguments objectAtIndex:1]; { //the second relative argument is the actual relative difference, so it must be an NSNumber if ([secondRelativeArgument expressionType] != NSConstantValueExpressionType) { goto errorExit; } if (![[secondRelativeArgument constantValue] isKindOfClass:[NSNumber class]]) { goto errorExit; } } } NSExpression * secondArgument = [outerCastArguments objectAtIndex:1]; { if ([secondArgument expressionType] != NSConstantValueExpressionType) { goto errorExit; } if (![[secondArgument constantValue] isEqual:@"NSDate"]) { goto errorExit; } } return DBL_MAX; errorExit: return 0.0; } This is a beast of a method. This is how we determine if this row template recognizes a predicate or not. Basically what we're doing here is making sure that the predicate has a extremely specific structure. If every requirement is met, then we return `DBL_MAX`, which is `NSPredicateEditorRowTemplate`'s way of saying "I match this predicate better than anyone else ever could". When `NSPredicateEditor` finds that multiple row templates match a predicate, it uses the row template that returns the highest match value. `DBL_MAX` is just a cheap way of ensuring that no other template can handle this predicate. The expected structure is as follows: (each box is an `NSExpression`, and downward arrows indicate arguments to a function expression) And yes, I use `goto` all over the place in that code. Deal with it. - (NSPredicate *) predicateWithSubpredicates:(NSArray *)subpredicates { NSPredicate * predicate = [super predicateWithSubpredicates:subpredicates]; if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { NSComparisonPredicate * comparison = (NSComparisonPredicate *)predicate; NSExpression * right = [comparison rightExpression]; NSNumber * value = [right constantValue]; NSInteger unit = [[self unitPopUpButton] indexOfSelectedItem]; NSTimeInterval newInterval = [self timeIntervalFromBase:[value doubleValue] unit:unit]; value = [NSNumber numberWithDouble:newInterval]; right = [NSExpression expressionForConstantValue:value]; //default values are for NSInTheLastPredicateOperatorType NSString * function = @"from:subtract:"; NSPredicateOperatorType newOperator = NSGreaterThanPredicateOperatorType; if ([comparison predicateOperatorType] == NSInTheNextPredicateOperatorType) { function = @"add:to:"; newOperator = NSLessThanPredicateOperatorType; } //"now()" is a function and takes no arguments NSExpression * now = [NSExpression expressionForFunction:@"now" arguments:[NSArray array]]; //CAST(now(), 'NSNumber') NSArray * castNowArguments = [NSArray arrayWithObjects:now, [NSExpression expressionForConstantValue:@"NSNumber"], nil]; NSExpression * castNow = [NSExpression expressionForFunction:@"castObject:toType:" arguments:castNowArguments]; //CAST(now(), 'NSNumber') [+/-] {a time interval} NSArray * relativeTimestampArguments = [NSArray arrayWithObjects:castNow, right, nil]; NSExpression * relativeTimestamp = [NSExpression expressionForFunction:function arguments:relativeTimestampArguments]; //CAST(CAST(now(), 'NSNumber') [+/-] {a time interval}, 'NSDate') NSArray * castToDateArguments = [NSArray arrayWithObjects:relativeTimestamp, [NSExpression expressionForConstantValue:@"NSDate"], nil]; NSExpression * castToDate = [NSExpression expressionForFunction:@"castObject:toType:" arguments:castToDateArguments]; predicate = [NSComparisonPredicate predicateWithLeftExpression:[comparison leftExpression] rightExpression:castToDate modifier:[comparison comparisonPredicateModifier] type:newOperator options:[comparison options]]; } return predicate; } This method is similar in principle to its counterpart in our earlier row template. Here we're letting `super` give us a predicate that contains what the user entered into the interval field, and then we extract that number, convert it to an absolute time interval, and then re-create the right expression of the comparison predicate. We create the expression manually to ensure that the structure returned here exactly matches the structure expected by `-matchForPredicate:`. Notice that we're changing our `matches` and `like` operators to the expected `less than` and `greater than` versions. - (void) setPredicate:(NSPredicate *)predicate { if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { NSComparisonPredicate * comparison = (NSComparisonPredicate *)predicate; /** I know that to get here, I must have matched the predicate. Therefore I can drill into the comparison predicate structure with reckless abandon! **/ NSExpression * right = [comparison rightExpression]; NSArray * arguments = [right arguments]; //this expression is our addition or subtraction function NSExpression * relative = [arguments objectAtIndex:0]; //the second argument to our relative function is our absolute time interval NSExpression * intervalExpression = [[relative arguments] objectAtIndex:1]; NSNumber * interval = [intervalExpression constantValue]; NSTimeInterval base = 0; NSInteger unit = 0; [self getBase:&base unit:&unit fromTimeInterval:[interval doubleValue]]; [[self unitPopUpButton] selectItemAtIndex:unit]; NSExpression * newRight = [NSExpression expressionForConstantValue:[NSNumber numberWithDouble:base]]; NSPredicateOperatorType newOperator = NSInTheLastPredicateOperatorType; if ([comparison predicateOperatorType] == NSLessThanPredicateOperatorType) { newOperator = NSInTheNextPredicateOperatorType; } predicate = [NSComparisonPredicate predicateWithLeftExpression:[comparison leftExpression] rightExpression:newRight modifier:[comparison comparisonPredicateModifier] type:newOperator options:[comparison options]]; } [super setPredicate:predicate]; } @end And finally! When given a predicate, we need to reflect those values in the UI. Here we're simply going to extract the absolute time interval that's buried in the `rightExpression` and use it to figure out what our base and unit should be. We also change the operator from `greater than` or `less than` to `likes` or `matches`, *simply so that the operator popup shows the right value*. After that, we re-package things, and send it on up to `super`. ## Wrap-up Not too bad, eh? Grokking this involves a little bit of mental gymnastics, but if you're familiar with predicates and their internal structure, most of this code should be really straight-forward. This was mainly to help connect all those disparate dots and form a coherent picture. The more you play with these row templates, the more you'll truly understand how they interact with each other and their parent predicate editor. They're quite clever little classes, and they can really make our apps that much more "Mac-like".