字符串表达式计算(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 计算核心代码
/// <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;
}
}
ParserInfixExpression 实现
在EvalNumber 方法的第一步调用了 ParserInfixExpression 方法来进行表达式的预处理
PreReplaceOpratorToText 如果变量中带有一些计算符号(+-*/()),通过这个方法转换临时变量,在获取值的时候再转换回来
/// <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;
}
PreParserInfixExpression 计算嵌套(),以及先行计算纯数字,主要是在后面转换为mongo语法的时候用到,让纯数字计算在内存中运行而不是数据库中计算
/// <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;
}
ParserInfixExpression 表达式转换核心代码
/// <summary>
/// 转换表达式
/// </summary>
/// <param name="expression"></param>
/// <param name="dynamicObject"></param>
/// <param name="isComplete"></param>
/// <returns></returns>
public Queue<EvalItem> ParserInfixExpression(string expression, Dictionary<string, object> dynamicObject, bool isComplete = false)
{
var queue = new Queue<EvalItem>();
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<OperatorChar>(); 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 核心代码
/// <summary>
/// 计算表达式的日期结果
/// </summary>
/// <param name="expression">表达式</param>
/// <param name="dynamicObject">动态对象</param>
/// <returns>计算的结果</returns>
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;
} /// <summary>
/// 毫秒级时间戳转成 DateTime
/// </summary>
/// <param name="unixTimestamp"></param>
/// <returns></returns>
private DateTime JsTimeToDateTime(long unixTimestamp)
{
return DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp).LocalDateTime;
}
代码中的数据定义
其他数据定义 OperatorChar EvalItem EItemType CharExtension 可以查看完整demo
相关说明
- 相关算法:中缀表达式,后缀表达式,逆波兰算法,调度场算法
- 相关语言:C#
- 参考项目:qinfengzhu/Evaluator
- 本文Demo: DevopsDemo/EvalDemo
后语
期间找了很多开源项目参考,需求的独特性,最终是实现了功能
整个计算字段的实现花了3周时间,终于是顺利上线。
沉迷学习,无法自拔。
字符串表达式计算(a+b/(a-b))的思路与实践的更多相关文章
- PHP 实现字符串表达式计算
什么是字符串表达式?即,将我们常见的表达式文本写到了字符串中,如:"$age >= 20",$age 的值是动态的整型变量. 什么是字符串表达式计算?即,我们需要一段程序来执 ...
- 使用堆栈结构进行字符串表达式("7*2-5*3-3+6/3")的计算
问题: 给定字符串String str = "7*2-5*3-3+6/3", 求出字符串里面表达式的结果? 像javascript有自带的eval()方法,可以直接计算.但java ...
- .NET平台开源项目速览(8)Expression Evaluator表达式计算组件使用
在文章:这些.NET开源项目你知道吗?让.NET开源来得更加猛烈些吧!(第二辑)中,给大家初步介绍了一下Expression Evaluator验证组件.那里只是概述了一下,并没有对其使用和强大功能做 ...
- 字符串表达式String Expressions
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [SAP ABAP开发技术总结]字符串表达式String Expressions
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- C# - 二叉树表达式计算
很早以前就写过双栈的表达式计算. 这次因为想深入学一下二叉树,网上都是些老掉牙的关于二叉树的基本操作. 感觉如果就学那些概念,没意思也不好记忆.于是动手写了一个表达式计算的应用例子. 这样学习印象才深 ...
- 转: c# 字符串公式计算
C# 自动计算字符串公式的值(三种方式) 从网络上找到这段源码,重新整理后测试通过. 有三种方式可自动计算字符串公式的值:1. 最简单的方式,由SQL语句计算2. 使用Microsoft.Javasc ...
- C++实现 逆波兰表达式计算问题
C++实现 逆波兰表达式计算问题 #include <iostream> #include <string> using namespace std; class Stack ...
- 第6.4节 Python动态表达式计算:eval函数详述
在Python动态执行的函数中,eval是用于执行表达式计算的函数,这个函数用于执行字符串中包含的一个表达式或其编译后对应的代码,不能适用于执行Python语句和完整的代码. 一. 语法 1. ...
- 第五周PTA笔记 后缀表达式+后缀表达式计算
后缀表达式 所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右进行(不用考虑运算符的优先级). 如:中缀表达式 3(5–2 ...
随机推荐
- 即构微信小程序直播组件是什么?有哪些功能?哪些小程序类目可以使用?
即构直播助手是微信官方认证的微信小程序插件,为开发者提供便捷.强大的微信小程序音视频直播服务. 即构直播助手除了包含微信小程序下的音视频推拉流能力,还支持iOS.Android.Windows.Web ...
- C标准库 操作文件
C标准库 操作文件 数据持久化的两种方法:文件和数据库 文本文件和二进制文件 举个例子,写C++的代码,源代码为文本文件.编译出来的可执行文件(.exe)文件是二进制文件 文本文件 以文本的编码(AS ...
- 2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: ‘t‘,运算结果为 true ‘f‘,运算结果为 false ‘!(subExpr
2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: 't',运算结果为 true 'f',运算结果为 false '!(subExpr ...
- Oracle快速拷贝数据
游标拷贝数据 根据条件进行数据拷贝 -- 游标方式拷贝数据 DECLARE CURSOR cur IS SELECT * FROM JACKPOT WHERE TO_CHAR(JACKPOT.CREA ...
- Centos7安装Python3.x
一.修改yum源 查看Centos发行版本 cat /etc/redhat-release 换阿里云yum源 备份原始yum源 mv /etc/yum.repos.d/CentOS-Base.repo ...
- Nginx:client_body_temp_path 指令的上传文件测试
结论 硬盘必须要有上传文件3倍大小的剩余空间.否则会报错"no space left on device". 需要注意,这3份数据都会写到硬盘.大文件上传,实时观察硬盘剩余空间wa ...
- XCTF-CGfsb
考察知识点 PWN.格式化字符串漏洞 题目链接 https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&am ...
- 【go语言】2.4.2 自定义包的创建和使用
在 Go 中,任何一个目录都可以被视为一个包.创建自定义包的基本步骤是: 新建一个目录,用于存放包的源文件. 在新建的目录中编写 Go 代码,代码的第一行应该是 package 包名. 使用 impo ...
- java反射newInstance()带删除线的问题
从java9开始,newInstance()方法不建议使用导致idea自动画了条删除横线 解决方法: //改用getDeclaredConstructor().newInstance() Object ...
- Azure Terraform(十四)Azure Key Vault 的机密管理
一,引言 最近有网友私信我,将 Terraform 部署到 Azure 是一种将基础结构作为代码进行管理的好方法,但是如何使用 Azure Key Vault 来存储我们的 Secret ?在这篇博文 ...