翼度科技»论坛 编程开发 .net 查看内容

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

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
前言

为满足业务需要,需要为项目中自定义模板添加一个计算字段的组件,通过设置字符串表达式,使用时在改变表达式其中一个字段的数据时,自动计算另外一个字段的值。
本篇为上篇,介绍原理,简单实现一个工具,输入字符串表达式,解析其中的参数,输入参数计算结果。
下篇将基于此封装实现对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 表达式转换核心代码
</ul>EvalDate 实现指定日期类型输出

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

  • EvalDate 核心代码

      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. }
      复制代码

代码中的数据定义

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

后语

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

来源:https://www.cnblogs.com/morang/archive/2023/10/31/csharp-eval.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

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

x

举报 回复 使用道具