Parsing and changing NSPredicate

2019-05-09 14:19发布

问题:

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);
}

回答1:

(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
    }
}