Превратите строку логического выражения в код .NET.

У меня есть логика, в которой клиент указывает строку, и мое приложение сообщает клиенту, присутствует ли эта строка в тексте, примерно так:

internal const string GlobalText = "blablabla";

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

В основном, если текст содержит переданную строку, верните true, иначе false.

Теперь я хочу сделать его более сложным. Допустим, если клиент передает строку "foo && bar", и мне нужно вернуть true, если этот текст содержит подстроки "foo" и "bar", простой подход:

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

person Romz    schedule 01.12.2016    source источник
comment
Что, если && является частью самой строки   -  person M.kazem Akhgary    schedule 02.12.2016
comment
Я бы сделал шаг назад, подумал о том, что вы пытаетесь сделать, и, если необходимо, посмотрел, не сдирал ли кто-нибудь шкуру с этого кота раньше.   -  person NPSF3000    schedule 02.12.2016
comment
Взгляните на шаблон проектирования интерпретатора: codeproject.com/articles/186183/interpreter -шаблон-дизайна   -  person Fruchtzwerg    schedule 02.12.2016
comment
@M.kazem Ахгари, мой текст не может содержать ничего, кроме букв и цифр, поэтому, если я вижу && или || в строке поиска я бы предположил, что это логическое выражение, и обработаю его соответствующим образом.   -  person Romz    schedule 02.12.2016
comment
Я бы предложил сделать это правильно и написать анализатор токенов. Особенно со скобками. Например, у вас есть вложенные логические значения и т. Д.? Анализатор токенов легко написать, и он будет обрабатывать все эти случаи... он также сообщит вам, где и в чем ошибка синтаксического анализа, тогда как ваша текущая реализация не может.   -  person SledgeHammer    schedule 02.12.2016
comment
Вы работаете с доменным языком, сделайте это правильно и напишите парсер/интерпретатор.   -  person Jeff Mercado    schedule 02.12.2016


Ответы (3)


Вы можете решить это универсально так же, как калькулятор или компилятор оценивает выражение:

  1. Маркируйте строку и идентифицируйте каждый маркер как оператор (OP) или операнд (A, B, C и т. д.).
  2. Преобразуйте последовательность маркеров из инфикса (A OP B) в постфикс (AB OP).
  3. Оцените последовательность маркеров постфикса.

Каждый из этих шагов может быть выполнен с помощью хорошо известного алгоритма на основе стека в линейном времени и пространстве. Кроме того, если вы используете этот метод, он автоматически распространяется на любые бинарные операторы, которые вы хотите добавить позже (сложение, вычитание, сопоставление нечетких строк и т. д. и т. д.).

Чтобы преобразовать инфикс в постфикс: http://scriptasylum.com/tutorials/infix_postfix/algorithms/infix-postfix/

Чтобы оценить постфикс: http://scriptasylum.com/tutorials/infix_postfix/algorithms/postfix-evaluation/

person Nick    schedule 02.12.2016

Самый простой способ сделать это — проанализировать входной текст и построить массив логических «истинных» значений, так что вы получите что-то вроде этого:

//Dictionary<string,List<string>> members;
members["Car"].Contains("BMW") // evals to 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
}

В качестве альтернативы, если вы можете найти метод "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";
  }
}

В этот момент уравнение «(Дэнни || Дженнифер) && (!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, обозначающую истину или ложь соответственно.

person jpreed00    schedule 02.12.2016
comment
Это разумно и позволяет избежать проблемы проецирования логики, выраженной строковым выражением, в код C#, вместо этого проецируя наличие или отсутствие таких токенов обратно в выражение. Я бы предположил, что использование else может быть не таким эффективным, как просто последовательное выполнение всех операторов if? Кроме того, вам не нужны скобки, кроме (1) и (0). - person ErikE; 02.12.2016
comment
@ErikE Я думаю, ты прав во всех отношениях. Я отредактирую. Я перечислил их как if-else, потому что в обычных циклах оценки у вас есть проблемы с порядком операций. Например, вы оцениваете операцию умножения, затем проваливаете операцию оценки суммы перед операцией деления (или чем-то еще), и вы получаете неправильный ответ. Хотя я не думаю, что здесь дело обстоит так. - person jpreed00; 02.12.2016
comment
Подождите, есть порядок операций для || и &&: сначала нужно выполнить &&. Я забираю свои слова о том, что не следует использовать if-else (хотя некоторые мысли могут позволить вам зацикливаться внутри цикла). Однако я поддерживаю то, что сказал о скобках. Например, (1||0)&&(1) НЕ равно 1||0&&1, потому что на самом деле это 1||(0&&1). - person ErikE; 02.12.2016
comment
@ErikE Если бы использовались соответствующие скобки, это не имело бы значения, но я все равно вернул его. :) - person jpreed00; 02.12.2016
comment
Лучше, чтобы случайные детишки из Интернета не злоупотребляли нашим кодом! Я согласен со всеми вашими правками. - person ErikE; 02.12.2016
comment
См. эту альтернативную реализацию. Что вы думаете? - person ErikE; 02.12.2016
comment
Это выглядит хорошо, хотя у вас есть ошибка в первой строке. У вас есть лишняя скобка в: @![01]). Моя реализация работает быстрее: dotnetfiddle.net/QG2cs6. Однако, если вы запускаете его достаточно, иногда ваш работает быстрее. Так что без понятия, что там происходит. Кроме того, я проверил только одно уравнение, в то время как различные уравнения могут давать разные результаты. - person jpreed00; 02.12.2016

С помощью DynamicExpresso вы можете легко сделать это за 10 строк. Допустим, текст и пользовательский ввод выглядят так:

var text = "Bob and Tom are in the same class.";
var input = "(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();
person Cheng Chen    schedule 02.12.2016