4

I have to migrate data from a previous app version to a new version. This also affects some predicates (NSPredicate instances) that were saved by users, which means that I have to change them programmatically.

Currently I try to parse the string I get with [NSPredicate predicateFormat] and change some expressions manually. For instance oldKeyPath == "something" to newKeyPath == "something". But it feels like a hack and I'm curious if there is a better way?

I read Apple's documentations about programming with NSPredicate and NSExpression. There are plenty methods to compose NSPredicate objects from NSExpressions. I was hoping to find the reverse way to get NSExpression objects from a NSPredicate. Am I missing something?

Thank you for any hint.

Solution

Thanks to Martin R I was able to make a category on NSPredicate that allows me to inject modifications to expressions.

@implementation NSPredicate (ExtractComparisions)

- (NSPredicate *)predicateByChangingComparisionsWithBlock:(NSPredicate *(^)(NSComparisonPredicate *))block {
  if ([self isKindOfClass: [NSCompoundPredicate class]]) {
    NSCompoundPredicate *compPred = (NSCompoundPredicate *)self;
    NSMutableArray *predicates = [NSMutableArray array];
    for (NSPredicate *predicate in [compPred subpredicates]) {
      NSPredicate *newPredicate = [predicate predicateByChangingComparisionsWithBlock: block];
      if (newPredicate != nil)
        [predicates addObject: newPredicate];
    }
    return [[[NSCompoundPredicate alloc] initWithType: compPred.compoundPredicateType
                                       subpredicates: predicates] autorelease];
  } if ([self isKindOfClass: [NSComparisonPredicate class]]) {
    return block((NSComparisonPredicate *)self);
  }
  return self;
}

@end

Here is a sample code on how to use it

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
  NSPredicate *predicate = [NSPredicate predicateWithFormat: @"key.path like %@ and count > 17", @"Hello"];
  NSPredicate *newPredicate = [predicate predicateByChangingComparisionsWithBlock:
                               ^NSPredicate *(NSComparisonPredicate *cp) {
    NSExpression *left = [cp leftExpression];
    NSExpression *right = [cp rightExpression];
    if ([[cp leftExpression] expressionType] == NSKeyPathExpressionType) {
      NSString *keyPath = [[cp leftExpression] keyPath];
      if ([keyPath isEqualToString: @"key.path"])
        left = [NSExpression expressionForKeyPath: @"key.new.path"];
      return [NSComparisonPredicate predicateWithLeftExpression: left
                                                rightExpression: right
                                                       modifier: cp.comparisonPredicateModifier
                                                           type: cp.predicateOperatorType
                                                        options:cp.options];
    }
    return cp;
  }];
  NSLog(@"Before: %@", predicate);
  NSLog(@"After: %@", newPredicate);
}
cocoafan
  • 4,884
  • 4
  • 37
  • 45
  • Nice! Note that you could also implement different category methods for each subclass NSCompoundPredicate and NSComparisonPredicate. That makes the isKindOfClass checks and casting obsolete (and looks sophisticated :-) – Martin R Oct 19 '13 at 13:35
  • yeah I thought about that but I realized that I still have to call the method on the superclass NSPredicate since I don't know the exact kind of class or I had to cast it first. So I made the compromise and "hide" the cast in the superclass. NSPredicate doesn't look like a class that we have to extend by subclasses anyway. But I still agree for the more sophisticated way. Maybe I change it later in my code. – – cocoafan Oct 20 '13 at 17:53

1 Answers1

3

(This is just an idea.) Every predicate is an instance of a subclass like NSCompoundPredicate or NSComparisonPredicate. So you can check the actual class of the predicate, cast it to an object of that class and then inspect its properties. If necessary, repeat the process with sub-predicates or expressions.

The following is just an example how a simple compound predicate could be "dissected" that way. It is not the general solution that checks for all cases, but may point you into the right direction.

NSPredicate *p0 = [NSPredicate predicateWithFormat:@"foo = 'bar' AND count > 17"];

if ([p0 isKindOfClass:[NSCompoundPredicate class]]) {
    NSCompoundPredicate *p0a = (NSCompoundPredicate *)p0;
    NSCompoundPredicateType type0 = p0a.compoundPredicateType;  // NSAndPredicateType
    NSPredicate *p1 = p0a.subpredicates[0];                     // foo = 'bar'
    NSPredicate *p2 = p0a.subpredicates[1];                     // count > 17
    if ([p1 isKindOfClass:[NSComparisonPredicate class]]) {
        NSComparisonPredicate *p1a = (NSComparisonPredicate *)p1;
        NSPredicateOperatorType type1 = p1a.predicateOperatorType;  // NSEqualToPredicateOperatorType
        NSExpression *e3 = p1a.leftExpression;                  // foo, NSKeyPathExpression
        NSExpression *e4 = p1a.rightExpression;                 // "bar", NSConstantValueExpression
    }
    if ([p2 isKindOfClass:[NSComparisonPredicate class]]) {
        NSComparisonPredicate *p2a = (NSComparisonPredicate *)p2;
        NSPredicateOperatorType type2 = p2a.predicateOperatorType;  // NSGreaterThanPredicateOperatorType
        NSExpression *e5 = p2a.leftExpression;                  // count, NSKeyPathExpression
        NSExpression *e6 = p2a.rightExpression;                 // 17, NSConstantValueExpression
    }
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thank you for showing the right direction! This is great and so simple. I will update my question to add my solution influenced by your answer. – cocoafan Oct 19 '13 at 11:21