|
前言
为满足业务需要,需要为项目中自定义模板添加一个计算字段的组件,通过设置字符串表达式,使用时在改变表达式其中一个字段的数据时,自动计算另外一个字段的值。
本篇为上篇,介绍原理,简单实现一个工具,输入字符串表达式,解析其中的参数,输入参数计算结果。
下篇将基于此封装实现对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 计算核心代码
- /// <summary>
- /// 计算表达式的计算结果
- /// </summary>
- /// <param name="expression">表达式</param>
- /// <param name="dynamicObject">动态对象</param>
- /// <param name="precision">精度 默认2</param>
- /// <returns>计算的结果</returns>
- 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<Expression>();
- 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.Lambda<Func<decimal>>(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;
- }
- }
复制代码
- PreParserInfixExpression 计算嵌套(),以及先行计算纯数字,主要是在后面转换为mongo语法的时候用到,让纯数字计算在内存中运行而不是数据库中计算
- /// <summary>
- /// 符号转换字典
- /// </summary>
- private static Dictionary<char, string> OperatorToTextDic = new Dictionary<char, string>()
- {
- { '+', "_JIA_" },
- { '-', "_JIAN_" },
- { '/', "_CHENG_" },
- { '*', "_CHU_" },
- { '(', "_ZKH_" },
- { ')', "_YKH_" }
- };
- /// <summary>
- /// 预处理参数符号转文本
- /// </summary>
- /// <param name="expression"></param>
- /// <param name="dynamicObject"></param>
- /// <returns></returns>
- 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;
- }
复制代码
ParserInfixExpression 表达式转换核心代码
</ul>EvalDate 实现指定日期类型输出
因项目需要,需要将当前日期,当前时间加入默认变量,并支持加入计算公式中,计算的结果也可以选择是日期或者数值。
需要实现这个功能,需要先定义好,时间如何计算,我们将日期时间转换成时间戳来进行转换后参与计算,计算完成后再转换成日期即可。
所以只需要在上面的数值计算包裹一层就可以得到日期的计算结果
- EvalDate 核心代码
- /// <summary>
- /// 预处理计算表达式
- /// </summary>
- /// <param name="expression">表达式</param>
- /// <param name="dynamicObject">参数</param>
- /// <param name="isCompile">是否是编译</param>
- /// <returns></returns>
- 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;
- }
复制代码
代码中的数据定义
其他数据定义 OperatorChar EvalItem EItemType CharExtension 可以查看完整demo
相关说明
后语
期间找了很多开源项目参考,需求的独特性,最终是实现了功能
整个计算字段的实现花了3周时间,终于是顺利上线。
沉迷学习,无法自拔。
来源:https://www.cnblogs.com/morang/archive/2023/10/31/csharp-eval.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|