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

这篇具有很好参考价值的文章主要介绍了字符串表达式计算(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+b/(a-b))的思路与实践

实现思路

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

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

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

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

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

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

实现代码

找了很多的开源项目,最终基于 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周时间,终于是顺利上线。

沉迷学习,无法自拔。文章来源地址https://www.toymoban.com/news/detail-729006.html

到了这里,关于字符串表达式计算(a+b/(a-b))的思路与实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • python 正则表达式提取字符串

    1、提取字符串的场景及公式、命令 背景 :目前遇到的场景主要是以某个字符串开始、某个字符串结束,提取中间部分的字符,有的时候需要开始的字符,有时不需要,大概涉及到了4种情况,场景及处理方式如下: 1.1 以某个字符开始、某个字符结束,期待的提取结果 包含

    2024年02月02日
    浏览(53)
  • 提取字符串中的最长数学表达式并计算(67%用例) C卷(Java&&Python&&C++&&Node.js&&C语言)

    提取字符串中的最长合法简单数学表达式,字符串长度最长的,并计算表达式的值。如果没有,则返回0 简单数学表达式只能包含以下内容 0-9数字,符号+-* 说明: 1.所有数字,计算结果都不超过long 2.如果有多个长度一样的,请返回第一个表达式的结果 3.数学表达式,必须是最

    2024年04月13日
    浏览(38)
  • notepad++ 正则表达式查找特定字符串

    批量文本的处理方法 在报文中有很多指标和值都具有固定的格式,比如是  a=\\\"1\\\" 这类格式,那么我们只取前面的指标a,就会比较复杂,而使用正则表达式就会快乐许多! 采用以下第二种方法 查找目标 =(.+?)\\\"    表示查找以等号开头,引号和空格  结尾的字符串,可以避免查

    2024年02月15日
    浏览(57)
  • java之字符串与正则表达式

    目录 String 构造方法 注意 格式控制字符串 常用方法 StringBuilder与StringBuffer 特点 理解可变与不可变 字符串拼接方法 字符串删除方法 字符串内插入字符 字符串替换方法 字符串反转方法 查字符串对应索引处的字符  截取字符串 正则表达式 正则表达式符号表 正则表达式常用方

    2023年04月22日
    浏览(45)
  • 【python】12.字符串和正则表达式

    正则表达式相关知识 在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要,正则表达式就是用于描述这些规则的工具,换句话说正则表达式是一种工具,它定义了字符串的匹配模式(如何检查一个字符串是否有跟某种模式匹配的部分或者从一个

    2024年01月16日
    浏览(49)
  • 正则表达式中 “$” 并不是表示 “字符串结束

    作者:Seth Larson 译者:豌豆花下猫@Python猫 英文:Regex character “$” doesn\\\'t mean “end-of-string” 转载请保留作者及译者信息! 这篇文章写一写我最近在用 Python 的正则表达式模块( re )开发 CPython 的 SBOM 工具时发现的一个令人惊讶的行为。 如果用过正则表达式,你可能知道 ^

    2024年04月15日
    浏览(39)
  • Python 自学(五) 之字符串及正则表达式

    目录 1. 字符串的分割合并  split()  join()         P132 2. 字符串的检索   count() find() index() startswith() endswith()         P134 3. 去除空格和特殊字符   strip()  lstrip() rstrip()          P139 4. 格式化字符串   format()         P142 5. 字符串编码转换  encode()  decode()        P145

    2024年01月25日
    浏览(51)
  • 【动态规划】【字符串】C++算法:正则表达式匹配

    视频算法专题 动态规划汇总 字符串 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘ ’ 的正则表达式匹配。 ‘.’ 匹配任意单个字符 \\\' ’ 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。 示例 1: 输入:

    2024年02月03日
    浏览(57)
  • 【Python习题集4】字符串与正则表达式

    1.输人一个字符串,将该字符串中下标为偶数的字符组成新串并通过字符串格式化方式显示。 (1)源代码 (2)运行结果截图 2.编写程序,生成一个由15个不重复的大小写字母组成的列表。 (1)源代码 (2)运行结果截图 3.给定字符串\\\"site sea suede sweet see kase sse sseeloses\\\",匹配出所有以

    2024年02月02日
    浏览(85)
  • 使用正则表达式 移除 HTML 标签后得到字符串

    在上述代码中,stripHTMLTags 函数使用正则表达式 /[^]+/g 来匹配所有的 HTML 标签,并使用空字符串进行替换,从而将 HTML 标签移除。 最后,返回移除 HTML 标签后的字符串。

    2024年02月14日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包