• 字符串表达式计算(a+b/(a-b))的思路与实践


    前言

    为满足业务需要,需要为项目中自定义模板添加一个计算字段的组件,通过设置字符串表达式,使用时在改变表达式其中一个字段的数据时,自动计算另外一个字段的值。

    本篇为上篇,介绍原理,简单实现一个工具,输入字符串表达式,解析其中的参数,输入参数计算结果。

    下篇将基于此封装实现对Mongo查询语法的封装,通过addFields的方式转换表达式,后续等封装成NuGet包再分享

    实现如下所示

    输入 1+1  输出 2
    输入 a+1 参数a:1 输出 2
    输入 (a+1)*b 输入a:1,b:1 输出 2
    输入 (a+1-(2+a)*3/3)/a+3 输入a:1 输出 2
    

    实现思路

    想要实现上面这个功能,需要先了解诸如 (a+1-(2+a)*3/3)/a+3 这个是什么?

    维基百科:中缀表示法(或中缀记法)是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间(例:3 + 4)。与前缀表达式(例:+ 3 4 )或后缀表达式(例:3 4 + )相比,中缀表达式不容易被电脑解析逻辑优先顺序,但仍被许多程序语言使用,因为它符合大多数自然语言的写法。

    前缀表示法 (+ 3 4 )也叫 波兰表示法

    后缀表示法 (3 4 + )也叫 逆波兰表示法

    在维基百科的说明中,也给出了和其相关的另外两种表示法,以及用于把中缀表达式转换到后缀表达式或树的算法:调度场算法 ,如下图所示

    实现代码

    找了很多的开源项目,最终基于 qinfengzhu/Evaluator ,实现了上述功能。

    调用代码

    using Evaluator;
    using System.Text.RegularExpressions;
    
    Console.WriteLine("字符串表达式计算工具");
    EvalTest();
    
    void EvalTest()
    {
        Console.WriteLine("----------------------------------------------------");
        var parse = new EvalParser();
        Console.Write("请输入表达式:");//a+b*3/5+a
        var evalStr = Console.ReadLine();
        if (string.IsNullOrEmpty(evalStr))
        {
            Console.WriteLine("Game Over");
            return;
        }
        //解析其中的变量并让用户输入
        var matchs = Regex.Matches(evalStr, @"\b[\w$]+\b");
        var paramsDic = new Dictionary<string, object>();
        //预定义参数
        paramsDic.Add("now_year", DateTime.Now.Year);
        paramsDic.Add("now_month", DateTime.Now.Month);
        paramsDic.Add("now_day", DateTime.Now.Day);
        foreach (Match match in matchs)
        {
            if (decimal.TryParse(match.Value, out decimal kp))
                continue;
            if (!paramsDic.ContainsKey(match.Value))
            {
                Console.Write($"请输入数字变量【{match.Value}】:");
                var paramValue = Console.ReadLine();
                decimal dvalue;
                while (!decimal.TryParse(paramValue, out dvalue))
                {
                    Console.WriteLine($"输入有误,请输入数字变量【{match.Value}】:");
                    paramValue = Console.ReadLine();
                }
                paramsDic.Add(match.Value, dvalue);
            }
        }
        var result = parse.EvalNumber(evalStr, paramsDic);
        Console.WriteLine($"结果:{result}");
        EvalTest();
    }
    

    EvalParser 类的实现

    通过上面调用代码可以看到,核心的计算类是 EvalParser ,调用其 EvalNumber 进行计算

    EvalNumber 实现

    • EvalNumber 方法,主要分为3步

      • 第一步将表达式解析转换到队列中,即将 中缀表达式,转换成后缀表达式
      • 第二步将队列中的表达式加入表达式栈中
      • 第三步使用表达式树进行计算
    • 返回值处理

      • 已知的错误有除以0和溢出的异常,所以直接捕获返回null,也可以在计算除数的时候判断值为0就直接返回null,
      • 精度处理
    • EvalNumber 计算核心代码

      • /// 
        /// 计算表达式的计算结果
        /// 
        /// 表达式
        /// 动态对象
        /// 精度 默认2
        /// 计算的结果
        public decimal? EvalNumber(string expression, Dictionary<string, object> dynamicObject, int precision = 2)
        {
            var values = dynamicObject ?? new Dictionary<string, object>();
            //中缀表达式,转换成后缀表达式并入列
            var queue = ParserInfixExpression(expression, values);
            var cacheStack = new Stack();
            while (queue.Count > 0)
            {
                var item = queue.Dequeue();
                if (item.ItemType == EItemType.Value && item.IsConstant)
                {
                    var itemExpression = Expression.Constant(item.Value);
                    cacheStack.Push(itemExpression);
                    continue;
                }
                if (item.ItemType == EItemType.Value && !item.IsConstant)
                {
                    var propertyName = item.Content.Trim();
                    //将参数替换回来
                    propertyName = PreReplaceTextToOprator(propertyName, values);
                    //参数为空的情况
                    if (!values.ContainsKey(propertyName) || values[propertyName] == null || !decimal.TryParse(values[propertyName].ToString(), out decimal propertyValue))
                        return null;
                    //var propertyValue = decimal.Parse(values[propertyName].ToString());
                    var itemExpression = Expression.Constant(propertyValue);
                    cacheStack.Push(itemExpression);
                }
                if (item.ItemType == EItemType.Operator)
                {
                    if (cacheStack.Count <= 1)
                        continue;
                    Expression firstParamterExpression = Expression.Empty();
                    Expression secondParamterExpression = Expression.Empty();
                    switch (item.Content[0])
                    {
                        case EvalParser.AddOprator:
                            firstParamterExpression = cacheStack.Pop();
                            secondParamterExpression = cacheStack.Pop();
                            var addExpression = Expression.Add(secondParamterExpression, firstParamterExpression);
                            cacheStack.Push(addExpression);
                            break;
                        case EvalParser.DivOperator:
                            firstParamterExpression = cacheStack.Pop();
                            secondParamterExpression = cacheStack.Pop();
                            var divExpression = Expression.Divide(secondParamterExpression, firstParamterExpression);
                            cacheStack.Push(divExpression);
                            break;
                        case EvalParser.MulOperator:
                            firstParamterExpression = cacheStack.Pop();
                            secondParamterExpression = cacheStack.Pop();
                            var mulExpression = Expression.Multiply(secondParamterExpression, firstParamterExpression);
                            cacheStack.Push(mulExpression);
                            break;
                        case EvalParser.SubOperator:
                            firstParamterExpression = cacheStack.Pop();
                            secondParamterExpression = cacheStack.Pop();
                            var subExpression = Expression.Subtract(secondParamterExpression, firstParamterExpression);
                            cacheStack.Push(subExpression);
                            break;
                        case EvalParser.LBraceOperator:
                        case EvalParser.RBraceOperator:
                            continue;
                        default:
                            throw new Exception("计算公式错误");
                    }
                }
            }
            if (cacheStack.Count == 0)
                return null;
            var lambdaExpression = Expression.Lambdadecimal>>(cacheStack.Pop());
            try
            {
                // 除0 溢出
                var value = lambdaExpression.Compile()();
                return Math.Round(value, precision);
            }
            catch (Exception ex)
            {
                //System.OverflowException
                //System.DivideByZeroException
                if (ex is DivideByZeroException
                    || ex is OverflowException)
                    return null;
                throw ex;
            }
        }
        

    ParserInfixExpression 实现

    • 在EvalNumber 方法的第一步调用了 ParserInfixExpression 方法来进行表达式的预处理

      • PreReplaceOpratorToText 如果变量中带有一些计算符号(+-*/()),通过这个方法转换临时变量,在获取值的时候再转换回来

        • 
          /// 
          /// 符号转换字典
          /// 
          private static Dictionary<char, string> OperatorToTextDic = new Dictionary<char, string>()
          {
              { '+', "_JIA_" },
              { '-', "_JIAN_" },
              { '/', "_CHENG_" },
              { '*', "_CHU_" },
              { '(', "_ZKH_" },
              { ')', "_YKH_" }
          };
          /// 
          /// 预处理参数符号转文本
          /// 
          /// 
          /// 
          /// 
          public string PreReplaceOpratorToText(string expression, Dictionary<string, object> dynamicObject)
          {
              //如果是参数里面包含了括号,将其中的参数替换成特殊字符
              var existOperatorKeys = dynamicObject.Keys.Where(s => OperatorToTextDic.Keys.Any(s2 => s.Contains(s2))).ToList();
              //存在特殊字符变量的
              if (existOperatorKeys.Any())
              {
                  //将符号替换成字母
                  foreach (var s in existOperatorKeys)
                  {
                      var newKey = s;
                      foreach (var s2 in OperatorToTextDic)
                      {
                          newKey = newKey.Replace(s2.Key.ToString(), s2.Value);
                      }
                      expression = expression.Replace(s, newKey);
                  }
              }
              return expression;
          }
          
      • PreParserInfixExpression 计算嵌套(),以及先行计算纯数字,主要是在后面转换为mongo语法的时候用到,让纯数字计算在内存中运行而不是数据库中计算

        • /// 
          /// 预处理计算表达式
          /// 
          /// 表达式
          /// 参数
          /// 是否是编译
          /// 
          public string PreParserInfixExpression(string expression, Dictionary<string, object> dynamicObject, bool isCompile = false)
          {
              expression = expression.Trim();
              string pattern = @"((.*?))";
              Match match = Regex.Match(expression, pattern);
              if (match.Success && match.Groups.Count > 1)
              {
                  var constText = match.Groups[0].Value;
                  var constValue = match.Groups[1].Value;
                  string numPattern = @"(([\s|0-9|+-*/|.]+))";
                  //纯数字计算 或者 不是编译预约
                  if (Regex.IsMatch(constText, numPattern) || !isCompile)
                  {
                      var evalValue = EvalNumber(constValue, dynamicObject);
                      if (evalValue == null)
                          return string.Empty;
                      var replaceText = evalValue.ToString();
                      expression = expression.Replace(constText, replaceText);
                  }
                  else if (isCompile)
                  {
                      //编译计算
                      var completeText = Compile(constValue, dynamicObject).ToString();
                      //临时参数Key
                      var tempPramKey = "temp_" + Guid.NewGuid().ToString("n");
                      dynamicObject.Add(tempPramKey, completeText);
                      expression = expression.Replace(constText, tempPramKey);
                  }
                  else
                  {
                      return expression;
                  }
                  return PreParserInfixExpression(expression, dynamicObject, isCompile);
              }
              return expression;
          }
          
    • ParserInfixExpression 表达式转换核心代码

      • /// 
        /// 转换表达式
        /// 
        /// 
        /// 
        /// 
        /// 
        public Queue ParserInfixExpression(string expression, Dictionary<string, object> dynamicObject, bool isComplete = false)
        {
            var queue = new Queue();
            if (string.IsNullOrEmpty(expression))
                return queue;
            expression = PreReplaceOpratorToText(expression, dynamicObject);
            expression = PreParserInfixExpression(expression, dynamicObject, isComplete);
            if (string.IsNullOrEmpty(expression))
                return queue;
            var operatorStack = new Stack();
        
            int index = 0;
            int itemLength = 0;
            //当第一个字符为+或者-的时候
            char firstChar = expression[0];
            if (firstChar == AddOprator || firstChar == SubOperator)
            {
                expression = string.Concat("0", expression);
            }
            int expressionLength = expression.Length;
            using (var scanner = new StringReader(expression))
            {
                string operatorPreItem = string.Empty;
                while (scanner.Peek() > -1)
                {
                    char currentChar = (char)scanner.Read();
                    switch (currentChar)
                    {
                        case AddOprator:
                        case SubOperator:
                        case DivOperator:
                        case MulOperator:
                        case LBraceOperator:
                        case RBraceOperator:
                            //直接把数字压入到队列中
                            operatorPreItem = expression.Substring(index, itemLength);
                            if (operatorPreItem != "")
                            {
                                var numberItem = new EvalItem(EItemType.Value, operatorPreItem);
                                queue.Enqueue(numberItem);
                            }
                            index = index + itemLength + 1;
                            itemLength = -1;
                            //当前操作符
                            var currentOperChar = new OperatorChar() { Operator = currentChar };
                            if (operatorStack.Count == 0)
                            {
                                operatorStack.Push(currentOperChar);
                                break;
                            }
                            //处理当前操作符与操作字符栈进出
                            var topOperator = operatorStack.Peek();
                            //若当前操作符为(或者栈顶元素为(则直接入栈
                            if (currentOperChar == LBraceOperatorChar || topOperator == LBraceOperatorChar)
                            {
                                operatorStack.Push(currentOperChar);
                                break;
                            }
                            //若当前操作符为),则栈顶元素顺序输出到队列,至到栈顶元素(输出为止,单(不进入队列,它自己也不进入队列
                            if (currentOperChar == RBraceOperatorChar)
                            {
                                while (operatorStack.Count > 0)
                                {
                                    if (operatorStack.Peek() != LBraceOperatorChar)
                                    {
                                        var operatorItem = new EvalItem(EItemType.Operator, operatorStack.Pop().GetContent());
                                        queue.Enqueue(operatorItem);
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                if (operatorStack.Count > 0 && operatorStack.Peek() == RBraceOperatorChar)
                                {
                                    operatorStack.Pop();
                                }
                                break;
                            }
                            //若栈顶元素优先级高于当前元素,则栈顶元素输出到队列,当前元素入栈
                            if (topOperator.Level > currentOperChar.Level || topOperator.Level == currentOperChar.Level)
                            {
                                var topActualOperator = operatorStack.Pop();
                                var operatorItem = new EvalItem(EItemType.Operator, topActualOperator.GetContent());
                                queue.Enqueue(operatorItem);
        
                                while (operatorStack.Count > 0)
                                {
                                    var tempTop = operatorStack.Peek();
                                    if (tempTop.Level > currentOperChar.Level || tempTop.Level == currentOperChar.Level)
                                    {
                                        var topTemp = operatorStack.Pop();
                                        var operatorTempItem = new EvalItem(EItemType.Operator, topTemp.GetContent());
                                        queue.Enqueue(operatorTempItem);
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                operatorStack.Push(currentOperChar);
                            }
                            //当当前元素小于栈顶元素的时候,当前元素直接入栈
                            else
                            {
                                operatorStack.Push(currentOperChar);
                            }
                            break;
                        default:
                            break;
                    }
                    itemLength++;
                }
            }
            //剩余无符号的字符串
            if (index < expressionLength)
            {
                string lastNumber = expression.Substring(index, expressionLength - index);
                var lastNumberItem = new EvalItem(EItemType.Value, lastNumber);
                queue.Enqueue(lastNumberItem);
            }
            //弹出栈中所有操作符号
            if (operatorStack.Count > 0)
            {
                while (operatorStack.Count != 0)
                {
                    var topOperator = operatorStack.Pop();
                    var operatorItem = new EvalItem(EItemType.Operator, topOperator.GetContent());
                    queue.Enqueue(operatorItem);
                }
            }
            return queue;
        }
        

    EvalDate 实现指定日期类型输出

    因项目需要,需要将当前日期,当前时间加入默认变量,并支持加入计算公式中,计算的结果也可以选择是日期或者数值。

    需要实现这个功能,需要先定义好,时间如何计算,我们将日期时间转换成时间戳来进行转换后参与计算,计算完成后再转换成日期即可。

    所以只需要在上面的数值计算包裹一层就可以得到日期的计算结果

    • EvalDate 核心代码

      • /// 
        /// 计算表达式的日期结果
        /// 
        /// 表达式
        /// 动态对象
        /// 计算的结果
        public DateTime? EvalDate(string expression, Dictionary<string, object> dynamicObject)
        {
            var dateNumValue = EvalNumber(expression, dynamicObject);
            if (dateNumValue == null)
                return null;
            if (long.TryParse(dateNumValue.ToString(), out long dateNum))
            {
                return JsTimeToDateTime(dateNum);
            }
            return null;
        }
        
        /// 
        /// 毫秒级时间戳转成 DateTime
        /// 
        /// 
        /// 
        private DateTime JsTimeToDateTime(long unixTimestamp)
        {
            return DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp).LocalDateTime;
        }
        

    代码中的数据定义

    其他数据定义 OperatorChar EvalItem EItemType CharExtension 可以查看完整demo

    相关说明

    后语

    期间找了很多开源项目参考,需求的独特性,最终是实现了功能

    整个计算字段的实现花了3周时间,终于是顺利上线。

    沉迷学习,无法自拔。

  • 相关阅读:
    《FAQ专场 | smardaten及应用软件的运维管理(上)》
    springcloudalibaba架构(28):分布式事务解决方案
    MongoDB 索引和常用命令
    GoJS 使用笔记
    Elasticsearch全文搜索技术之二kibana的简介和使用
    windows平台goldgate同步oracle数据库
    2023年中国大学生留学现状及未来发展规划分析:直接就业仍是毕业后的主流选择[图]
    微信小程序Day3笔记
    [附源码]计算机毕业设计基于Springboot的手机电商网站
    LeetCode(力扣)90. 子集 IIPython
  • 原文地址:https://www.cnblogs.com/morang/p/csharp-eval.html