使用 NSPredicate 分析字符串

发布于 2024-11-10 11:08:41 字数 7587 浏览 0 评论 0 原文

你好!这个问题很长,所以一定要先坐下来:)

在我的代码中的某个时刻,我从用户那里得到一个字符串,然后我分析该字符串。我所说的分析是指遍历每个字符并根据用户在 NSPredicateEditor 中设置的谓词对其进行数学计算。谓词是这样以编程方式设置的:

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];

所以你可以看到我有三个关键路径和一些可以与它们进行比较的常量。 在分析字符串时,我基本上想用伪代码执行此操作:

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

您可以看到我的问题基本上出在哪里:

  • 'Character Before/After' 由于空格而不能成为键路径,但我想保持这种方式对于用户来说更漂亮(想象一下有一些像“characterBefore”的东西......)

  • 常量实际上代表像“0123456789”这样的字符串,但我也无法向用户显示

我能够找到解决此问题的方法,但我现在它不适用于每个字符,而且它也非常效率低下(至少在我看来)。我所做的就是从谓词中获取谓词格式,替换我必须替换的内容,然后评估新格式。现在,一些真实的代码可以解释这一点:

#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!
    }
}

所以,给你,这是我正在做的事情的简化版本。如果您看到任何拼写错误,很可能它们不在我的代码中,因为我对此进行了相当多的编辑,因此它会更简单。

总结一下:

  • 我需要评估用户对许多字符所做的谓词,对其进行大量修改,同时尝试尽可能高效
  • 我的方法发现的问题是:

    • 我认为它一点也不干净
    • 我不能保证它适用于每种情况(当其中一个字符是换行符/输入字符时,谓词会引发错误,指出它无法理解格式)

这就是大家!感谢您阅读到目前为止。愿你的神与你同在,解决这个烂摊子!

编辑:只是为了澄清相关问题,我想补充一点,似乎触发此问题的事实是,当我设置谓词编辑器时,我不能在一开始就定义一个带有名称的常量(显示给用户) ) 和一个表示该常量并以谓词格式插入的值。键路径也是如此:如果我可以有一个显示名称,然后有一个值,即谓词的 var 字符串($VAR 或其他任何东西),那么所有问题都将得到解决。如果可能的话,请告诉我如何做。如果不可能,那么请关注我描述的其他问题。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

二智少女 2024-11-17 11:08:41

所以主要问题是:

您可以构建一个使用与用户界面中显示的不同常量的 NSPredicateEditorRowTemplate 吗?

答:是的。您过去可以在 Interface Builder 中进行设置,但我在 Xcode 4 中尝试这样做并不成功。该功能可能已在 IB => 中删除。 Xcode 4 合并。

幸运的是,您可以用代码来完成。不幸的是,它需要一个 NSPredicateEditorRowTemplate 子类。我还没有找到没有这样的子类的方法。

要点是覆盖行模板中的 -templateViews ,并执行如下操作:

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

瞧!谓词编辑器中的弹出窗口,其中标题与键路径不匹配。

编辑

解决此问题的另一种方法(无需子类化!)是将自定义文本放入 .strings 文件中,并将编辑器“本地化”为英语(或您想要的任何语言)。


想象一下这种情况:用户将这个表达式“Character/is not/Number”放入谓词编辑器中。该谓词的格式将是“字符!=“0123456789”这始终是正确的,即使字符是数字!我如何“替换”这些运算符:is/is not 与它们的实际函数 IN/NOT(IN . ..)?

如果您要在谓词中表达这些比较,您可能不会使用 !===。喜欢:

character MATCHES '[0-9]'

还有一个of:

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

为此,我能想到的方法是使用两个单独的行模板,以第一种格式识别并显示谓词(MATCHES '[0-9]')。 ),第二个将识别并显示否定,您开始进入 NSPredicateEditorRowTemplates 的奇怪领域,我认为我仍然不确定您想要完成什么。为什么你想将字符串的每个字符与谓词进行匹配,但无论如何,

有关如何创建自定义行模板的更多信息,请查看这两篇博客文章:

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:

给我一枪 2024-11-17 11:08:41

为什么设置视觉文本需要子类? 会不会更便宜,如下所示:

    - (void)setVisibleMenuTitlesInPredicate:(NSPredicateEditorRowTemplate *)predicateTemplate
    {
        NSArray *predicateTemplateViews = [predicateTemplate templateViews];
        NSPopUpButton *leftMenu = [predicateTemplateViews objectAtIndex:0];
        NSArray *leftMenuList = [leftMenu itemArray];
        for (NSMenuItem *menuItem in leftMenuList) {
            id keyPathValue = [menuItem representedObject];
            if ([[keyPathExpression keyPath] isEqual:@"before"]) {
                  [menuItem setTitle:@"Character Before"];
                } else if ([[keyPathExpression keyPath] isEqual:@"after"]) {
                  [menuItem setTitle:@"Character After"];
                }
    }

简单地在类中创建一个方法(例如:然后在 setRowTemplates: 之前调用它)

    [self setVisibleMenuTitlesInPredicate:predicateTemplateA];

Why is the subclass necessary for setting the visual text? Wouldn't it be less expensive to simply create a method in your class like:

    - (void)setVisibleMenuTitlesInPredicate:(NSPredicateEditorRowTemplate *)predicateTemplate
    {
        NSArray *predicateTemplateViews = [predicateTemplate templateViews];
        NSPopUpButton *leftMenu = [predicateTemplateViews objectAtIndex:0];
        NSArray *leftMenuList = [leftMenu itemArray];
        for (NSMenuItem *menuItem in leftMenuList) {
            id keyPathValue = [menuItem representedObject];
            if ([[keyPathExpression keyPath] isEqual:@"before"]) {
                  [menuItem setTitle:@"Character Before"];
                } else if ([[keyPathExpression keyPath] isEqual:@"after"]) {
                  [menuItem setTitle:@"Character After"];
                }
    }

And then call it before setRowTemplates: like so:

    [self setVisibleMenuTitlesInPredicate:predicateTemplateA];
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文