вторник, 30 октября 2012 г.

Парсер для логических выражений



Не так давно я столкнулся с необходимостью релизации вычисления значения логических выржений представленных в виде обыкновенной строки. Логическое выражение может быть простым, например b > 5 или же составным, и состоять из нескольких груп, как показано ниже.
a <> 0 & (b > 5 | c <> 0) & (d < 5 & e = "test")
В целях формализации  данной задачи, я написал юнит-тест который демонстрирует что конкретно я хочу. Код теста прдставлен ниже.
        [TestMethod]
        public void Demo_Test()
        {
            var statementBody = "a <> 0 & (b > 5 | c <> 0) & (d < 5 & e = \"test\")";
            var dic = new Dictionary<string, string>
                          {
                              { "a", "10" },  { "b", "1" }, { "c", "1" }, { "d", "2" }, { "e", "test" }
                          };

            var checker = new ConditionChecker();
            var result = checker.Check( statementBody , dic);

            Assert.IsTrue(result);
        }
Классический способ решения данной задачи состоит в том, что по выражению формируется дерево, осуществляются вычисления для каждого узла дерева и формируется результат.
Решение которое мне пришло в голову показалось мне  более простым и достаточно эффективным и я решил что это именно то что мне нужно. Суть его заключалась в том, что вместо того что бы по этому выражению стоить дерево, достаточно найти все условные операции в выражении вычислить результат для каждой из них и в выражении вместо условной операции поместить результат. То есть в итоге прийти к следующему выражению которое будет эквивалентным исходному: 
 "1 & (0 | 1) & (1 & 1)"
Здесь 1 – означает что условие выполняется, а 0 –что нет. То есть условие a <> 0 при a = 10 выполняется и поэтому замещаем его единицей, а при b = 1 условие b > 5 не выполняется поэтому замещаем его нулем, И таким же образом поступаем со всеми остальными операциями.
После того как все условия были посчитаны и замещены нулем или единицей, следующим этапом будет вычисления результата логических операций в скобках, то есть сначала вычисляем результат для (0 | 1), потом для (1 & 1), а потом для т ого что получилось 1 & 1 & 1. В итоге получим требуемый результат.
Ниже представлена реализация данного способа в деталях.
        private const char groupStart = '(';
        private const char groupEnd = ')';

        private const char logicalAnd = '&';
        private const char logicalOr = '|';


        private const string operatorEqual = "=";
        private const string operatorGreate = ">";
        private const string operatorLess = "<";
        private const string operatorNotEqual = "<>";


        internal bool Check(string content, Dictionary<string, string> variables)
        {
            var operations = new[] { operatorEqual, operatorNotEqual, operatorGreate, operatorLess };
            var dic = new List<CalculatedCondition>();

            content = groupStart + content + groupEnd;

            foreach (var operation in operations)
            {
                var index = 0;
                do
                {
                    index = content.IndexOf(operation, index);
                    if(index == -1)
                    {
                        break;
                    }

                    if(!dic.Any(c => index > c.BeginIndex && index < c.EndIndex))
                    {
                        int beginIndex = GetExpressionBeginBoundary(content, index, operation.Length);
                        int endIndex = GetExpressionEndBoundary(content, index, operation.Length);

                        var expression = content.Substring(beginIndex, endIndex - beginIndex);
                        var result = this.CheckCondition(expression, variables);

                        dic.Add(new CalculatedCondition { BeginIndex = beginIndex, EndIndex = endIndex, Expression = expression, Result = result });
                    }

                    index += operation.Length;

                } while(true);
            }

            content = dic.Aggregate(content, (current, calculatedCondition) => current.Replace(calculatedCondition.Expression, calculatedCondition.Result ? "1" : "0"));
            content = content.Replace(" ", string.Empty);

            return ComposeResult(content);
        }

Вычисление условия:
        internal bool CheckCondition(string condition, Dictionary<string, string> variables)
        {
            var result = false;

            if (condition.IndexOfAny(new[] { logicalAnd, logicalOr }) != -1)
            {
                // only expressions like "varname > value" are supported
                throw new NotSupportedException();  
            }

            var operations = new [] { operatorEqual, operatorNotEqual, operatorGreate, operatorLess};

            foreach (var operation in operations)
            {
                if(condition.Contains(operation))
                {
                    int index = condition.IndexOf(operation);
                    var verb = condition.Substring(0, index).Trim();
                   
                    var arg = condition.Substring(index + operation.Length).Trim(new [] {' ','\'','\"' });

                    string val = null;
                    if(variables.ContainsKey(verb))
                    {
                       val = variables[verb];
                    }

                    if(val !=null)
                    {
                        decimal num1 = 0, num2 = 0;
                        bool isNum1 = decimal.TryParse(val, out num1);
                        bool isNum2 = decimal.TryParse(arg, out num2);
                       
                        switch (operation)
                        {
                            case operatorEqual:
                                result = isNum1 && isNum2 ? num1 == num2 : val == arg;
                                break;
                            case operatorNotEqual:
                                result = isNum1 && isNum2 ? num1 != num2 : val != arg;
                                break;
                            case operatorGreate:
                                result = !isNum1 || !isNum2 || num1 > num2;
                                break;
                            case operatorLess:
                                result = !isNum1 || !isNum2 || num1 < num2;
                                break;
                        }
                        break;
                    }
                    else
                    {
                        throw new NotSupportedException(
string.Format("Variable {0} doesn't exists in the current context", verb));
                    }
                }
            }
           
            return result;
        }

Подготовка конечного результата:
        private bool ComposeResult(string content)
        {
            // (0|(1&1))&1)

            Debug.WriteLine(content);

            var start = 0;
            var resultChanged = false;

            do
            {
                start = 0;
                resultChanged = false;

                while(true)
                {
                    start = content.IndexOf(groupStart, start);

                    var nextStart = content.IndexOf(groupStart, start + 1);
                    var end = content.IndexOf(groupEnd, start + 1);

                    if (start == -1 && end == -1)
                    {
                        break;
                    } 
                    else
                    {
                        if(end < nextStart || nextStart == -1)
                        {
                            var str = content.Substring(start, (end + 1) - start);
                            bool result = ToBool(str);
                            content = content.Replace(str, result ? "1" : "0");
                            resultChanged = true;
                            break;
                        }

                    }

                    start = nextStart;
                }
               
            }while(resultChanged && start != -1);

            return content == "1";
        }

Вычисление границ выражения:
        internal int GetExpressionBeginBoundary(string content, int operationIndex, int operationLength)
        {
            int resultIndex = -1;
            var index = operationIndex - 1;
            while (content.ElementAt(index) == ' ')
            {
                --index;
            }

            for (; index >= 0; --index)
            {
                var separatorsForNotStrings = new[] { ' ', groupStart, logicalAnd, logicalOr };
                var currentElement = content.ElementAt(index);
                if (groupEnd == currentElement)
                {
                    throw new NotSupportedException(string.Format("Illegal endGroup symbol found at {0}", index));
                }

                if (separatorsForNotStrings.Any(c => c == currentElement))
                {
                    resultIndex = index + 1;
                    break;
                }
            }

            // set begin on expression
            if (resultIndex == -1)
            {
                resultIndex = 0;
            }

            return resultIndex;
        }

        internal int GetExpressionEndBoundary(string content, int operationIndex, int operationLength)
        {
            int resultIndex = -1;
            // skip operation text
            var index = operationIndex + operationLength;
            // skip spaces if present
            while (content.ElementAt(index) == ' ')
            {
                ++index;
            }

            // check if operand is string
            bool isString = false;
            var argumentStartsWith = content.ElementAt(index);
            if(argumentStartsWith == '\'' || argumentStartsWith == '\"')
            {
                isString = true;
                ++index;
            }

            for(; index < content.Length; index++)
            {
                var separatorsForNotStrings = new[] { ' ', groupEnd, logicalAnd, logicalOr };
                var separatorsForStrings = new[] { '\'', '\"' };

                var currentElement = content.ElementAt(index);

                if(groupStart == currentElement)
                {
                    throw new NotSupportedException(string.Format("Illegal startGroup symbol found at {0}", index));
                }

                if(isString)
                {
                    if (separatorsForStrings.Any(c => c == currentElement))
                    {
                        ++index;
                        resultIndex = index;
                        break;
                    }
                }
                else
                {
                    if (separatorsForNotStrings.Any(c => c == currentElement))
                    {
                        resultIndex = index;
                        break;
                    }
                }
            }

            // set end on expression
            if (resultIndex == -1)
            {
                resultIndex = content.Length;
            }

            return resultIndex;

        }
Вспомогательные функции:
        private bool ToBool(string str)
        {
            str = str.Trim(new[] { groupStart, groupEnd });

            var operations = new[] {  logicalAnd, logicalOr };

            if(!operations.Any(o => str.Contains(o)))
            {
                return str == "1";
            }

            var arg1 = str.ElementAt(0) == '1';
            for(int i=1; iLength; i+=2)
            {
                var operation = str.ElementAt(i);
                var arg2 = str.ElementAt(i+1) == '1';

                switch (operation)
                {
                    case logicalAnd:
                        arg1 = arg1 & arg2;
                        break;
                    case logicalOr:
                        arg1 = arg1 | arg2;
                        break;
                }
            }

            return arg1;
        }