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

打印 上一主题 下一主题

主题 888|帖子 888|积分 2666

前言

为满足业务需要,需要为项目中自定义模板添加一个计算字段的组件,通过设置字符串表达式,使用时在改变表达式其中一个字段的数据时,自动计算另外一个字段的值。
本篇为上篇,介绍原理,简单实现一个工具,输入字符串表达式,解析其中的参数,输入参数计算结果。
下篇将基于此封装实现对Mongo查询语法的封装,通过addFields的方式转换表达式,后续等封装成NuGet包再分享
实现如下所示
  1. 输入 1+1  输出 2
  2. 输入 a+1 参数a:1 输出 2
  3. 输入 (a+1)*b 输入a:1,b:1 输出 2
  4. 输入 (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 ,实现了上述功能。
调用代码
  1. using Evaluator;
  2. using System.Text.RegularExpressions;
  3. Console.WriteLine("字符串表达式计算工具");
  4. EvalTest();
  5. void EvalTest()
  6. {
  7.     Console.WriteLine("----------------------------------------------------");
  8.     var parse = new EvalParser();
  9.     Console.Write("请输入表达式:");//a+b*3/5+a
  10.     var evalStr = Console.ReadLine();
  11.     if (string.IsNullOrEmpty(evalStr))
  12.     {
  13.         Console.WriteLine("Game Over");
  14.         return;
  15.     }
  16.     //解析其中的变量并让用户输入
  17.     var matchs = Regex.Matches(evalStr, @"\b[\w$]+\b");
  18.     var paramsDic = new Dictionary<string, object>();
  19.     //预定义参数
  20.     paramsDic.Add("now_year", DateTime.Now.Year);
  21.     paramsDic.Add("now_month", DateTime.Now.Month);
  22.     paramsDic.Add("now_day", DateTime.Now.Day);
  23.     foreach (Match match in matchs)
  24.     {
  25.         if (decimal.TryParse(match.Value, out decimal kp))
  26.             continue;
  27.         if (!paramsDic.ContainsKey(match.Value))
  28.         {
  29.             Console.Write($"请输入数字变量【{match.Value}】:");
  30.             var paramValue = Console.ReadLine();
  31.             decimal dvalue;
  32.             while (!decimal.TryParse(paramValue, out dvalue))
  33.             {
  34.                 Console.WriteLine($"输入有误,请输入数字变量【{match.Value}】:");
  35.                 paramValue = Console.ReadLine();
  36.             }
  37.             paramsDic.Add(match.Value, dvalue);
  38.         }
  39.     }
  40.     var result = parse.EvalNumber(evalStr, paramsDic);
  41.     Console.WriteLine($"结果:{result}");
  42.     EvalTest();
  43. }
复制代码
EvalParser 类的实现

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


  • EvalNumber 方法,主要分为3步

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

  • 返回值处理

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

  • EvalNumber 计算核心代码

      1. /// <summary>
      2. /// 计算表达式的计算结果
      3. /// </summary>
      4. /// <param name="expression">表达式</param>
      5. /// <param name="dynamicObject">动态对象</param>
      6. /// <param name="precision">精度 默认2</param>
      7. /// <returns>计算的结果</returns>
      8. public decimal? EvalNumber(string expression, Dictionary<string, object> dynamicObject, int precision = 2)
      9. {
      10.     var values = dynamicObject ?? new Dictionary<string, object>();
      11.     //中缀表达式,转换成后缀表达式并入列
      12.     var queue = ParserInfixExpression(expression, values);
      13.     var cacheStack = new Stack<Expression>();
      14.     while (queue.Count > 0)
      15.     {
      16.         var item = queue.Dequeue();
      17.         if (item.ItemType == EItemType.Value && item.IsConstant)
      18.         {
      19.             var itemExpression = Expression.Constant(item.Value);
      20.             cacheStack.Push(itemExpression);
      21.             continue;
      22.         }
      23.         if (item.ItemType == EItemType.Value && !item.IsConstant)
      24.         {
      25.             var propertyName = item.Content.Trim();
      26.             //将参数替换回来
      27.             propertyName = PreReplaceTextToOprator(propertyName, values);
      28.             //参数为空的情况
      29.             if (!values.ContainsKey(propertyName) || values[propertyName] == null || !decimal.TryParse(values[propertyName].ToString(), out decimal propertyValue))
      30.                 return null;
      31.             //var propertyValue = decimal.Parse(values[propertyName].ToString());
      32.             var itemExpression = Expression.Constant(propertyValue);
      33.             cacheStack.Push(itemExpression);
      34.         }
      35.         if (item.ItemType == EItemType.Operator)
      36.         {
      37.             if (cacheStack.Count <= 1)
      38.                 continue;
      39.             Expression firstParamterExpression = Expression.Empty();
      40.             Expression secondParamterExpression = Expression.Empty();
      41.             switch (item.Content[0])
      42.             {
      43.                 case EvalParser.AddOprator:
      44.                     firstParamterExpression = cacheStack.Pop();
      45.                     secondParamterExpression = cacheStack.Pop();
      46.                     var addExpression = Expression.Add(secondParamterExpression, firstParamterExpression);
      47.                     cacheStack.Push(addExpression);
      48.                     break;
      49.                 case EvalParser.DivOperator:
      50.                     firstParamterExpression = cacheStack.Pop();
      51.                     secondParamterExpression = cacheStack.Pop();
      52.                     var divExpression = Expression.Divide(secondParamterExpression, firstParamterExpression);
      53.                     cacheStack.Push(divExpression);
      54.                     break;
      55.                 case EvalParser.MulOperator:
      56.                     firstParamterExpression = cacheStack.Pop();
      57.                     secondParamterExpression = cacheStack.Pop();
      58.                     var mulExpression = Expression.Multiply(secondParamterExpression, firstParamterExpression);
      59.                     cacheStack.Push(mulExpression);
      60.                     break;
      61.                 case EvalParser.SubOperator:
      62.                     firstParamterExpression = cacheStack.Pop();
      63.                     secondParamterExpression = cacheStack.Pop();
      64.                     var subExpression = Expression.Subtract(secondParamterExpression, firstParamterExpression);
      65.                     cacheStack.Push(subExpression);
      66.                     break;
      67.                 case EvalParser.LBraceOperator:
      68.                 case EvalParser.RBraceOperator:
      69.                     continue;
      70.                 default:
      71.                     throw new Exception("计算公式错误");
      72.             }
      73.         }
      74.     }
      75.     if (cacheStack.Count == 0)
      76.         return null;
      77.     var lambdaExpression = Expression.Lambda<Func<decimal>>(cacheStack.Pop());
      78.     try
      79.     {
      80.         // 除0 溢出
      81.         var value = lambdaExpression.Compile()();
      82.         return Math.Round(value, precision);
      83.     }
      84.     catch (Exception ex)
      85.     {
      86.         //System.OverflowException
      87.         //System.DivideByZeroException
      88.         if (ex is DivideByZeroException
      89.             || ex is OverflowException)
      90.             return null;
      91.         throw ex;
      92.     }
      93. }
      复制代码

  • PreParserInfixExpression 计算嵌套(),以及先行计算纯数字,主要是在后面转换为mongo语法的时候用到,让纯数字计算在内存中运行而不是数据库中计算

      1. /// <summary>
      2. /// 符号转换字典
      3. /// </summary>
      4. private static Dictionary<char, string> OperatorToTextDic = new Dictionary<char, string>()
      5. {
      6.     { '+', "_JIA_" },
      7.     { '-', "_JIAN_" },
      8.     { '/', "_CHENG_" },
      9.     { '*', "_CHU_" },
      10.     { '(', "_ZKH_" },
      11.     { ')', "_YKH_" }
      12. };
      13. /// <summary>
      14. /// 预处理参数符号转文本
      15. /// </summary>
      16. /// <param name="expression"></param>
      17. /// <param name="dynamicObject"></param>
      18. /// <returns></returns>
      19. public string PreReplaceOpratorToText(string expression, Dictionary<string, object> dynamicObject)
      20. {
      21.     //如果是参数里面包含了括号,将其中的参数替换成特殊字符
      22.     var existOperatorKeys = dynamicObject.Keys.Where(s => OperatorToTextDic.Keys.Any(s2 => s.Contains(s2))).ToList();
      23.     //存在特殊字符变量的
      24.     if (existOperatorKeys.Any())
      25.     {
      26.         //将符号替换成字母
      27.         foreach (var s in existOperatorKeys)
      28.         {
      29.             var newKey = s;
      30.             foreach (var s2 in OperatorToTextDic)
      31.             {
      32.                 newKey = newKey.Replace(s2.Key.ToString(), s2.Value);
      33.             }
      34.             expression = expression.Replace(s, newKey);
      35.         }
      36.     }
      37.     return expression;
      38. }
      复制代码

ParserInfixExpression 表达式转换核心代码

    1. /// <summary>
    2. /// 预处理计算表达式
    3. /// </summary>
    4. /// <param name="expression">表达式</param>
    5. /// <param name="dynamicObject">参数</param>
    6. /// <param name="isCompile">是否是编译</param>
    7. /// <returns></returns>
    8. public string PreParserInfixExpression(string expression, Dictionary<string, object> dynamicObject, bool isCompile = false)
    9. {
    10.     expression = expression.Trim();
    11.     string pattern = @"((.*?))";
    12.     Match match = Regex.Match(expression, pattern);
    13.     if (match.Success && match.Groups.Count > 1)
    14.     {
    15.         var constText = match.Groups[0].Value;
    16.         var constValue = match.Groups[1].Value;
    17.         string numPattern = @"(([\s|0-9|+-*/|.]+))";
    18.         //纯数字计算 或者 不是编译预约
    19.         if (Regex.IsMatch(constText, numPattern) || !isCompile)
    20.         {
    21.             var evalValue = EvalNumber(constValue, dynamicObject);
    22.             if (evalValue == null)
    23.                 return string.Empty;
    24.             var replaceText = evalValue.ToString();
    25.             expression = expression.Replace(constText, replaceText);
    26.         }
    27.         else if (isCompile)
    28.         {
    29.             //编译计算
    30.             var completeText = Compile(constValue, dynamicObject).ToString();
    31.             //临时参数Key
    32.             var tempPramKey = "temp_" + Guid.NewGuid().ToString("n");
    33.             dynamicObject.Add(tempPramKey, completeText);
    34.             expression = expression.Replace(constText, tempPramKey);
    35.         }
    36.         else
    37.         {
    38.             return expression;
    39.         }
    40.         return PreParserInfixExpression(expression, dynamicObject, isCompile);
    41.     }
    42.     return expression;
    43. }
    复制代码
</ul>EvalDate 实现指定日期类型输出

因项目需要,需要将当前日期,当前时间加入默认变量,并支持加入计算公式中,计算的结果也可以选择是日期或者数值。
需要实现这个功能,需要先定义好,时间如何计算,我们将日期时间转换成时间戳来进行转换后参与计算,计算完成后再转换成日期即可。
所以只需要在上面的数值计算包裹一层就可以得到日期的计算结果

  • EvalDate 核心代码

      1. /// <summary>
      2. /// 转换表达式
      3. /// </summary>
      4. /// <param name="expression"></param>
      5. /// <param name="dynamicObject"></param>
      6. /// <param name="isComplete"></param>
      7. /// <returns></returns>
      8. public Queue<EvalItem> ParserInfixExpression(string expression, Dictionary<string, object> dynamicObject, bool isComplete = false)
      9. {
      10.     var queue = new Queue<EvalItem>();
      11.     if (string.IsNullOrEmpty(expression))
      12.         return queue;
      13.     expression = PreReplaceOpratorToText(expression, dynamicObject);
      14.     expression = PreParserInfixExpression(expression, dynamicObject, isComplete);
      15.     if (string.IsNullOrEmpty(expression))
      16.         return queue;
      17.     var operatorStack = new Stack<OperatorChar>();
      18.     int index = 0;
      19.     int itemLength = 0;
      20.     //当第一个字符为+或者-的时候
      21.     char firstChar = expression[0];
      22.     if (firstChar == AddOprator || firstChar == SubOperator)
      23.     {
      24.         expression = string.Concat("0", expression);
      25.     }
      26.     int expressionLength = expression.Length;
      27.     using (var scanner = new StringReader(expression))
      28.     {
      29.         string operatorPreItem = string.Empty;
      30.         while (scanner.Peek() > -1)
      31.         {
      32.             char currentChar = (char)scanner.Read();
      33.             switch (currentChar)
      34.             {
      35.                 case AddOprator:
      36.                 case SubOperator:
      37.                 case DivOperator:
      38.                 case MulOperator:
      39.                 case LBraceOperator:
      40.                 case RBraceOperator:
      41.                     //直接把数字压入到队列中
      42.                     operatorPreItem = expression.Substring(index, itemLength);
      43.                     if (operatorPreItem != "")
      44.                     {
      45.                         var numberItem = new EvalItem(EItemType.Value, operatorPreItem);
      46.                         queue.Enqueue(numberItem);
      47.                     }
      48.                     index = index + itemLength + 1;
      49.                     itemLength = -1;
      50.                     //当前操作符
      51.                     var currentOperChar = new OperatorChar() { Operator = currentChar };
      52.                     if (operatorStack.Count == 0)
      53.                     {
      54.                         operatorStack.Push(currentOperChar);
      55.                         break;
      56.                     }
      57.                     //处理当前操作符与操作字符栈进出
      58.                     var topOperator = operatorStack.Peek();
      59.                     //若当前操作符为(或者栈顶元素为(则直接入栈
      60.                     if (currentOperChar == LBraceOperatorChar || topOperator == LBraceOperatorChar)
      61.                     {
      62.                         operatorStack.Push(currentOperChar);
      63.                         break;
      64.                     }
      65.                     //若当前操作符为),则栈顶元素顺序输出到队列,至到栈顶元素(输出为止,单(不进入队列,它自己也不进入队列
      66.                     if (currentOperChar == RBraceOperatorChar)
      67.                     {
      68.                         while (operatorStack.Count > 0)
      69.                         {
      70.                             if (operatorStack.Peek() != LBraceOperatorChar)
      71.                             {
      72.                                 var operatorItem = new EvalItem(EItemType.Operator, operatorStack.Pop().GetContent());
      73.                                 queue.Enqueue(operatorItem);
      74.                             }
      75.                             else
      76.                             {
      77.                                 break;
      78.                             }
      79.                         }
      80.                         if (operatorStack.Count > 0 && operatorStack.Peek() == RBraceOperatorChar)
      81.                         {
      82.                             operatorStack.Pop();
      83.                         }
      84.                         break;
      85.                     }
      86.                     //若栈顶元素优先级高于当前元素,则栈顶元素输出到队列,当前元素入栈
      87.                     if (topOperator.Level > currentOperChar.Level || topOperator.Level == currentOperChar.Level)
      88.                     {
      89.                         var topActualOperator = operatorStack.Pop();
      90.                         var operatorItem = new EvalItem(EItemType.Operator, topActualOperator.GetContent());
      91.                         queue.Enqueue(operatorItem);
      92.                         while (operatorStack.Count > 0)
      93.                         {
      94.                             var tempTop = operatorStack.Peek();
      95.                             if (tempTop.Level > currentOperChar.Level || tempTop.Level == currentOperChar.Level)
      96.                             {
      97.                                 var topTemp = operatorStack.Pop();
      98.                                 var operatorTempItem = new EvalItem(EItemType.Operator, topTemp.GetContent());
      99.                                 queue.Enqueue(operatorTempItem);
      100.                             }
      101.                             else
      102.                             {
      103.                                 break;
      104.                             }
      105.                         }
      106.                         operatorStack.Push(currentOperChar);
      107.                     }
      108.                     //当当前元素小于栈顶元素的时候,当前元素直接入栈
      109.                     else
      110.                     {
      111.                         operatorStack.Push(currentOperChar);
      112.                     }
      113.                     break;
      114.                 default:
      115.                     break;
      116.             }
      117.             itemLength++;
      118.         }
      119.     }
      120.     //剩余无符号的字符串
      121.     if (index < expressionLength)
      122.     {
      123.         string lastNumber = expression.Substring(index, expressionLength - index);
      124.         var lastNumberItem = new EvalItem(EItemType.Value, lastNumber);
      125.         queue.Enqueue(lastNumberItem);
      126.     }
      127.     //弹出栈中所有操作符号
      128.     if (operatorStack.Count > 0)
      129.     {
      130.         while (operatorStack.Count != 0)
      131.         {
      132.             var topOperator = operatorStack.Pop();
      133.             var operatorItem = new EvalItem(EItemType.Operator, topOperator.GetContent());
      134.             queue.Enqueue(operatorItem);
      135.         }
      136.     }
      137.     return queue;
      138. }
      复制代码

代码中的数据定义

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

后语

期间找了很多开源项目参考,需求的独特性,最终是实现了功能
整个计算字段的实现花了3周时间,终于是顺利上线。
沉迷学习,无法自拔。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南七星之家

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表