Search code examples
objective-cstringnspredicatenspredicateeditor

Using NSPredicate to analyze strings


Howdy! This question is pretty long so be sure to have a seat first :)

At one point in my code I get a string from the user, and I analyze that string. By analyzing I mean going through every character and mathcing it against a predicate the user has set in an NSPredicateEditor. The predicate is setup progamatically this way:

NSArray* keyPaths = [NSArray arrayWithObjects:
                         [NSExpression expressionForKeyPath: @"Character"],
                         [NSExpression expressionForKeyPath: @"Character Before"],
                         [NSExpression expressionForKeyPath: @"Character After"],
                         nil];
// -----------------------------------------
NSArray* constants = [NSArray arrayWithObjects:
                      [NSExpression expressionForConstantValue: @"Any letter"],
                      [NSExpression expressionForConstantValue: @"Letter (uppercase)"],
                      [NSExpression expressionForConstantValue: @"Letter (lowercase)"],
                      [NSExpression expressionForConstantValue: @"Number"],
                      [NSExpression expressionForConstantValue: @"Alphanumerical"],
                      [NSExpression expressionForConstantValue: @"Ponctuation Mark"],
                      nil];
// -----------------------------------------
NSArray* compoundTypes = [NSArray arrayWithObjects:
                          [NSNumber numberWithInteger: NSNotPredicateType],
                          [NSNumber numberWithInteger: NSAndPredicateType],
                          [NSNumber numberWithInteger: NSOrPredicateType],
                          nil];
// -----------------------------------------
NSArray* operatorsA = [NSArray arrayWithObjects:
                       [NSNumber numberWithInteger: NSEqualToPredicateOperatorType],
                       [NSNumber numberWithInteger: NSNotEqualToPredicateOperatorType],
                       nil];

NSArray* operatorsB = [NSArray arrayWithObjects:
                       [NSNumber numberWithInteger: NSInPredicateOperatorType],
                       nil];
// -----------------------------------------
NSPredicateEditorRowTemplate* template1 = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions: keyPaths
                                                                                       rightExpressions: constants
                                                                                               modifier: NSDirectPredicateModifier 
                                                                                              operators: operatorsA
                                                                                                options: 0];

NSPredicateEditorRowTemplate* template2 = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions: keyPaths
                                                                           rightExpressionAttributeType: NSStringAttributeType
                                                                                               modifier: NSDirectPredicateModifier 
                                                                                              operators: operatorsB
                                                                                                options: 0];

NSPredicateEditorRowTemplate* compound = [[NSPredicateEditorRowTemplate alloc] initWithCompoundTypes: compoundTypes];
// -----------------------------------------
NSArray* rowTemplates = [NSArray arrayWithObjects: template1, template2, compound, nil];

[myPredicateEditor setRowTemplates: rowTemplates];

So you can see I have three keypaths and some constants they can be compared with. When analyzing the string I basically want to do this, in pseudocode:

originalString = [string from NSTextView]
for (char in originalString)
    bChar = [character before char]
    aChar = [character after char]

    predicate = [predicate from myPredicateEditor]

    // using predicate - problem!
    result = [evaluate predicate with:
               bChar somehow 'linked' to keypath 'Character Before'
               char 'linked' to 'Character'
               aChar 'linked' to 'Character After' // These values change, of course
              and also:
               constant "All letters" as "abc...WXYZ"
               constant "Numbers" as "0123456789"
               etc for all other constants set up  // These don't
              ]

    if (result) then do something with char, bChar and aChar

You can see where my problem basically lies:

  • 'Character Before/After' cannot be keypaths because of the space, but I want to keep it that way as it is more beautiful for the user (imagine having something as 'characterBefore' instead...)

  • Constants such as 'Numbers' actually represent strings like '0123456789', witch I can't display to the user as well

I was able to find a workaround to this problem, but I now it doesn't work with every character and it is also very unefficient (in my opinion, at least). What I do is get the predicate format from the predicate, replace what I have to replace, and evaluate that new format instead. Now for some real code that explains this:

#define kPredicateSubstitutionsDict [NSDictionary dictionaryWithObjectsAndKeys: \
@"IN 'abcdefghijklmnopqrstuvwxyz'", @"== \"Letter (lowercase)\"", \
@"IN 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'", @"== \"Letter (uppercase)\"", \
@"IN 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'", @"== \"Any letter\"", \
@"IN '1234567890'", @"== \"Number\"", \
@"IN 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'", @"== \"Alphanumerical\"", \
@"IN ',.;:!?'", @"== \"Ponctuation Mark\"", \
\
@"MATCHES '[^a-z]'"         , @"!= \"Letter (lowercase)\"", \
@"MATCHES '[^A-Z]'"         , @"!= \"Letter (uppercase)\"", \
@"MATCHES '[^a-zA-Z]'"      , @"!= \"Any letter\"", \
@"MATCHES '[^0-9]'"         , @"!= \"Number\"", \
@"MATCHES '[^a-zA-Z0-9]'"   , @"!= \"Alphanumerical\"", \
@"MATCHES '[^,\.;:!\?]'"  , @"!= \"Ponctuation Mark\"", \
\
nil]

// NSPredicate* predicate is setup in another place
// NSString* originalString is also setup in another place
NSString* predFormat = [predicate predicateFormat];
for (NSString* key in [kPredicateSubstitutionsDict allKeys]) {
    prefFormat = [predFormat stringByReplacingOccurrencesOfString: key withString: [kPredicateSubstitutionsDict objectForKey: key]];
}

for (NSInteger i = 0; i < [originalString length]; i++) {
    NSString* charString = [originalString substringWithRange: NSMakeRange(i, 1)];
    NSString* bfString;
    NSString* afString;

    if (i == 0) {
            bfString = @"";
    }
    else {
            bfString = [originalString substringWithRange: NSMakeRange(i - 1, 1)];
    }

    if (i == [originalString length] - 1) {
            afString = @"";
    }
    else {
            afString = [originalString substringWithRange: NSMakeRange(i + 1, 1)];
    }

    predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character Before" withString: [NSString stringWithFormat: @"\"%@\"", bfString]];
    predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character After" withString: [NSString stringWithFormat: @"\"%@\"", afString]];
    predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character" withString: [NSString stringWithFormat: @"\"%@\"", charString]];

    NSPredicate* newPred = [NSPredicate predicateWithFormat: predFormat];
    if ([newPred evaluateWithObject: self]) { // self just so I give it something (nothing is actually gotten from self)
        // if predicate evaluates to true, then do something exciting!
    }
}

So, here you go, this is a simplified version of what I am doing. If you see any typos, most probably they're not in my code, because I've edited this quite a bit so it would be simpler.

To summarize:

  • I need to evaluate the predicate the user makes against many characters, modifying it quite a bit, while trying to be as efficient as possible
  • The problems I find with my approach are:

    • I don't think it's clean at all
    • I have no guarantee that it will work on every case (when one of the character is a newline/enter character, the predicate raises an error saying it can't understand the format)

That's all folks! Thanks for reading thus far. May your god be with you when solving this mess!

EDIT: Just to clarify things abut, I would add that what seems the trigger to this problem is the fact that I cannot, right at the start when I setup the predicate editor, define one constant with a name (that gets displayed to the user) and a value that represents that constant and gets inserted in the predicate format. The same thing for keypaths: if I could have one display name, and then one value that would be those var strings for predicate ($VAR or whatever it is) all the problems would be solved. If this is possible, please tell me how. If it is impossible, then please focus on the other problems I describe.


Solution

  • So the main question:

    Can you build an NSPredicateEditorRowTemplate that uses different constants than what's displayed in the user interface?

    Answer: yes. You used to be able to set this up in Interface Builder, but my attempts to do so in Xcode 4 have been unsuccessful. It's possible that the capability was removed in the IB => Xcode 4 merge.

    Fortunately, you can do it in code. Unfortunately, it requires an NSPredicateEditorRowTemplate subclass. I haven't found a way to do it without such a subclass.

    The gist is to override -templateViews in your row template, and do something like this:

    - (NSArray *)templateViews {
      NSArray *views = [super templateViews];
      // views[0] is the left-hand popup
      // views[1] is the operator popup
      // views[2] is the right-hand side (textfield, popup, whatever)
      NSPopUpButton *left = [views objectAtIndex:0];
      NSArray *items = [left itemArray];
      for (NSMenuItem *item in items) {
        //the representedObject of the item is the expression that will be used in the predicate
        NSExpression *keyPathExpression = [item representedObject];
    
        //we can alter the -title of the menuItem without altering the expression object
        if ([[keyPathExpression keyPath] isEqual:@"before"]) {
          [item setTitle:@"Character Before"];
        } else if ([[keyPathExpression keyPath] isEqual:@"after"]) {
          [item setTitle:@"Character After"];
        }
      }
      return views;
    }
    

    And voilá! A popup in your predicate editor where the title does not match the keypath.

    edit

    The other way to solve this (without subclassing!) would be to put your custom text in a .strings file and "localize" your editor to English (or whatever language you want).


    Imagine this situation: a user puts in the predicate editor this expression Character/is not/Number. The format for that predicate would be "character != "0123456789" This is always true, even if the character is a number! How do I "replace" these operators: is/is not with their real functions IN/NOT (IN ...)?

    If you were to express these comparisons in a predicate, you probably wouldn't use != and ==. Likely, you'd want to do something like:

    character MATCHES '[0-9]'
    

    And one of:

    character MATCHES '[^0-9]'
    NOT(character MATCHES '[0-9]')
    

    For this, the way that I can think of to do this would be to have two separate row templates. One would recognize and display predicates in the first format (MATCHES '[0-9]'), and the second would recognize and display the negation. You're starting to get into the weird realm of NSPredicateEditorRowTemplates, I think. I'm still not really sure what you're trying to accomplish and why you want to be matching every character of a string against a predicate, but whatever.

    For more information on how to create custom row templates, check out these two blog posts: