将布尔表达式字符串转换为 .NET 代码

Turn boolean-expression string into the .NET code

我的逻辑是客户指定一个字符串,我的应用程序会告诉客户该字符串是否出现在文本中,如下所示:

internal const string GlobalText = "blablabla";

bool PresentInTheText(string searchString)
{
  return GlobalText.IndexOf(searchString, StringComparison.OrdinalIgnoreCase) >= 0;
}

基本上如果文本包含传递的字符串 return true 否则为 false。

现在我想让它更复杂。假设客户传递了一个字符串 "foo && bar",如果此文本同时包含 "foo""bar" 子字符串,我需要 return true,直接方法:

bool result;
if (!string.IsNullOrEmpty(passedExpression) && 
passedExpression.Contains(" && "))
{
    var tokens = passedExpression.Split(new[] { " && " }, StringSplitOptions.RemoveEmptyEntries);
    result = true;
    foreach (var token in tokens)
    {
        if (GlobalText.IndexOf(token, StringComparison.OrdinalIgnoreCase) < 0)
        {
            result = false;
        }
    }
}
return result;

它适用于像 A && B && C 这样的表达式。但我想推广解决方案以支持所有布尔运算符。 比方说:("foo" && "bar") || "baz"。解决方案是什么?

我会说获取传递的字符串,使用正则表达式添加到所有字符串 .IndexOf(token, StringComparison.OrdinalIgnoreCase) < >= 0 代码,它会是这样的:

("foo".IndexOf(token, StringComparison.OrdinalIgnoreCase) < >= 0 && 
"bar".IndexOf(token, StringComparison.OrdinalIgnoreCase) < >= 0)) ||
"baz".IndexOf(token, StringComparison.OrdinalIgnoreCase) < >= 0

然后把这个字符串变成一个函数,用Reflections执行。最好的解决方案是什么?

预计到达时间:

测试用例:

bool Contains(string text, string expressionString);

string text = "Customers: David, Danny, Mike, Luke. Car: BMW"

string str0 = "Luke"
string str1 = "(Danny || Jennifer) && (BMW)"
string str2 = "(Mike && BMW) || Volvo"
string str3 = "(Mike || David) && Ford"
string str4 = "David && !BMW"

bool Contains(string text, string str0);  //True - This text contains "Luke"
bool Contains(string text, string str1);  //True - David and BMW in the text
bool Contains(string text, string str2);  //True - Mike and BMW in the text
bool Contains(string text, string str3);  //False - no Ford in the list
bool Contains(string text, string str4);  //False - BMW in the list

最简单的方法是解析输入文本并构建一个布尔值数组 "true",因此您最终会得到如下结果:

//Dictionary<string,List<string>> members;
members["Car"].Contains("BMW") // evals to True;

或者,如果任何输入条目之间没有功能差异(即,只要单词出现在输入文本中,变量的计算结果为 true),您可能只构建一个字符串列表,而不是让担心使用他们的分类作为字典键。

然后,您解析方程式字符串并查看布尔列表中是否存在这些值,如果存在,则将它们在原始方程式字符串中替换为 1。如果不存在,则将它们替换为一个 0.

你最终得到的结果看起来像这样:

string str0 = "Luke" // "1"
string str1 = "(Danny || Jennifer) && (BMW)" // "(1 || 0) && (1)"
string str2 = "(Mike && BMW) || Volvo" // "(1 && 1) || 0"
string str3 = "(Mike || David) && Ford" // "(1 || 1) && 0"
string str4 = "David && !BMW" // "1 && !0"

现在,它只是一个简单的迭代字符串替换。您在字符串上循环,直到剩下的唯一东西是 1 或 0。

while (str.Length > 1)
{
  if (str.Contains("(1 || 1)"))
    str.Replace("(1 || 1)", "1");
  if (str.Contains("(1 || 0)"))
    str.Replace("(1 || 0)", "1");
  // and so on
}

或者,如果你能找到 C# "eval" 方法,你可以直接计算表达式(你也可以使用 True/False 而不是 0/1)。

编辑:

找到一个可能适用于解析测试方程的简单分词器:

using System;
using System.Text.RegularExpressions;

public static string[] Tokenize(string equation)
{
    Regex RE = new Regex(@"([\(\)\! ])");
    return (RE.Split(equation));
}
//from here: https://www.safaribooksonline.com/library/view/c-cookbook/0596003390/ch08s07.html

编辑 2: 刚刚写了一个示例项目来实现它。

//this parses out the string input, does not use the classifications
List<string> members = new List<string>();
string input = "Customers: David, Danny, Mike, Luke. Car: BMW";
string[] t1 = input.Split(new string[] {". "},         StringSplitOptions.RemoveEmptyEntries);
foreach (String t in t1)
{
  string[] t2 = t.Split(new string[] { ": " }, StringSplitOptions.RemoveEmptyEntries);
  string[] t3 = t2[1].Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
  foreach (String s in t3)
  {
    members.Add(s.Trim());
  }
}

这会将等式标记化并替换为 1 和 0。

string eq = "(Danny || Jennifer) && (!BMW)";
Regex RE = new Regex(@"([\(\)\! ])");
string[] tokens = RE.Split(eq);
string eqOutput = String.Empty;
string[] operators = new string[] { "&&", "||", "!", ")", "("};
foreach (string tok in tokens)
{
  if (tok.Trim() == String.Empty)
    continue;
  if (operators.Contains(tok))
  {
    eqOutput += tok;
  }
  else if (members.Contains(tok))
  {
    eqOutput += "1";
  }
  else 
  {
    eqOutput += "0";
  }
}

此时,等式“(Danny || Jennifer) && (!BMW)”看起来像“(1||0)&&(!1)”。

现在将等式简化为 1 或 0。

while (eqOutput.Length > 1)
{
  if (eqOutput.Contains("!1"))
    eqOutput = eqOutput.Replace("!1", "0");
  else if (eqOutput.Contains("!0"))
    eqOutput = eqOutput.Replace("!0", "1");
  else if (eqOutput.Contains("1&&1"))
    eqOutput = eqOutput.Replace("1&&1", "1");
  else if (eqOutput.Contains("1&&0"))
    eqOutput = eqOutput.Replace("1&&0", "0");
  else if (eqOutput.Contains("0&&1"))
    eqOutput = eqOutput.Replace("0&&1", "0");
  else if (eqOutput.Contains("0&&0"))
    eqOutput = eqOutput.Replace("0&&0", "0");
  else if (eqOutput.Contains("1||1"))
    eqOutput = eqOutput.Replace("1||1", "1");
  else if (eqOutput.Contains("1||0"))
    eqOutput = eqOutput.Replace("1||0", "1");
  else if (eqOutput.Contains("0||1"))
    eqOutput = eqOutput.Replace("0||1", "1");
  else if (eqOutput.Contains("0||0"))
    eqOutput = eqOutput.Replace("0||0", "0");
  else if (eqOutput.Contains("(1)"))
    eqOutput = eqOutput.Replace("(1)", "1");
  else if (eqOutput.Contains("(0)"))
    eqOutput = eqOutput.Replace("(0)", "0");
}

现在您应该有一个仅包含 1 或 0 的字符串,分别表示 true 或 false。

您可以用与计算器或编译器计算表达式相同的方式解决这个问题:

  1. 标记字符串并将每个标记标识为运算符 (OP) 或操作数(A、B、C 等)。
  2. 将令牌序列从中缀 (A OP B) 转换为后缀 (A B OP)。
  3. 评估后缀标记序列。

这些步骤中的每一个都可以使用众所周知的基于堆栈的算法在线性时间和 space 内完成。另外,如果您使用此方法,它会自动扩展到您以后要添加的任何二元运算符(加法、减法、模糊字符串匹配等)。

从中缀转换为后缀:http://scriptasylum.com/tutorials/infix_postfix/algorithms/infix-postfix/

评估后缀: http://scriptasylum.com/tutorials/infix_postfix/algorithms/postfix-evaluation/

DynamicExpresso 的帮助下,您可以在 10 行内轻松完成此操作。假设文本和用户输入是这样的:

var text = "Bob and Tom are in the same class.";
var input = "(Bob || Alice) && Tom";

你可以认为"Bob""Alice""Tom"是变量,其类型是bool C#,用户输入的字符串成为一个有效的 C# 表达式,使用 DynamicExpresso 计算它并得到一个 bool 结果。

var variables = input.Split(new[] { "(", "||", "&&", ")", " " }, 
    StringSplitOptions.RemoveEmptyEntries);

var interpreter = new Interpreter();

foreach (var variable in variables)
{
    interpreter.SetVariable(variable, text.Contains(variable));
}

var result = (bool)interpreter.Parse(input).Invoke();