侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

使用java处理字符串公式运算的方法

Java  /  管理员 发布于 8年前   145

  在改进一个关于合同的项目时,有个需求,就是由于合同中非数据项的计算公式会根据年份而进行变更,而之前是将公式硬编码到系统中的,只要时间一变,系统就没法使用了,因此要求合同中各个非基础数据的项都能自定义公式,根据设置的公式来自动生成报表和合同中的数据。

  显然定义的公式都是以字符串来存储到数据库的,可是java中没有这种执行字符串公式的工具或者类,而且是公式可以嵌套一个中间公式。比如:基础数据dddd是56,而一个公式是依赖dddd的,eeee=dddd*20,而最终的公式可能是这样:eeee*-12+13-dddd+24。可知eeee是一个中间公式,所以一个公式的计算需要知道中间公式和基础数据。

这好像可以使用一个解释器模式来解决,但是我没有成功,因为括号的优先级是一个棘手的问题,后来又想到可以使用freemarker类似的模板引擎或者java6之后提供的ScriptEngine 脚本引擎,做了个实验,脚本引擎可以解决,但是这限制了必须使用java6及以上的版本。最终功夫不负有心人,终于找到了完美解决方案,即后缀表达式。我们平时写的公式称作中缀表达式,计算机处理起来比较困难,所以需要先将中缀表达式转换成计算机处理起来比较容易的后缀表达式。

将中缀表达式转换为后缀表达式具体算法规则:见后缀表达式


   a.若为 '(',入栈;

   b.若为 ')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ;

   c.若为 除括号外的其他运算符 ,当其优先级高于栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。

   ・当扫描的中缀表达式结束时,栈中的的所有运算符出栈; 

我们提出的要求设想是这样的:
复制代码 代码如下:
public class FormulaTest {
     @Test
     public void testFormula() {
         //基础数据
         Map<String, BigDecimal> values = new HashMap<String, BigDecimal>();
         values.put("dddd", BigDecimal.valueOf(56d));

         //需要依赖的其他公式
         Map<String, String> formulas = new HashMap<String, String>();
         formulas.put("eeee", "#{dddd}*20");

         //需要计算的公式
         String expression = "#{eeee}*-12+13-#{dddd}+24";

         BigDecimal result = FormulaParser.parse(expression, formulas, values);
         Assert.assertEquals(result, BigDecimal.valueOf(-13459.0));
     }
 }

以下就是解决问题的步骤:

1、首先将所有中间变量都替换成基础数据

FormulaParser的finalExpression方法会将所有的中间变量都替换成基础数据,就是一个递归的做法
复制代码 代码如下:
public class FormulaParser {
     /**
      * 匹配变量占位符的正则表达式
      */
     private static Pattern pattern = Pattern.compile("\\#\\{(.+?)\\}");

     /**
      * 解析公式,并执行公式计算
      *
      * @param formula
      * @param formulas
      * @param values
      * @return
      */
     public static BigDecimal parse(String formula, Map<String, String> formulas, Map<String, BigDecimal> values) {
         if (formulas == null)formulas = Collections.emptyMap();
         if (values == null)values = Collections.emptyMap();
         String expression = finalExpression(formula, formulas, values);
         return new Calculator().eval(expression);
     }

     /**
      * 解析公式,并执行公式计算
      *
      * @param formula
      * @param values
      * @return
      */
     public static BigDecimal parse(String formula, Map<String, BigDecimal> values) {
         if (values == null)values = Collections.emptyMap();
         return parse(formula, Collections.<String, String> emptyMap(), values);
     }

     /**
      * 解析公式,并执行公式计算
      *
      * @param formula
      * @return
      */
     public static BigDecimal parse(String formula) {
         return parse(formula, Collections.<String, String> emptyMap(), Collections.<String, BigDecimal> emptyMap());
     }

     /**
      * 将所有中间变量都替换成基础数据
      *
      * @param expression
      * @param formulas
      * @param values
      * @return
      */
     private static String finalExpression(String expression, Map<String, String> formulas, Map<String, BigDecimal> values) {
         Matcher m = pattern.matcher(expression);
         if (!m.find())return expression;

         m.reset();

         StringBuffer buffer = new StringBuffer();
         while (m.find()) {
             String group = m.group(1);
             if (formulas != null && formulas.containsKey(group)) {
                 String formula = formulas.get(group);
                 m.appendReplacement(buffer, '(' + formula + ')');
             } else if (values != null && values.containsKey(group)) {
                 BigDecimal value = values.get(group);
                 m.appendReplacement(buffer,value.toPlainString());
             }else{
                 throw new IllegalArgumentException("expression '"+expression+"' has a illegal variable:"+m.group()+",cause veriable '"+group+"' not being found in formulas or in values.");
             }
         }
         m.appendTail(buffer);
         return finalExpression(buffer.toString(), formulas, values);
     }
 }

2、将中缀表达式转换为后缀表达式

  Calculator的infix2Suffix将中缀表达式转换成了后缀表达式

3、计算后缀表达式

  Calculator的evalInfix计算后缀表达式
复制代码 代码如下:
public class Calculator{
     private static Log logger = LogFactory.getLog(Calculator.class);

     /**
      * 左括号
      */
     public final static char LEFT_BRACKET = '(';

     /**
      * 右括号
      */
     public final static char RIGHT_BRACKET = ')';

     /**
      * 中缀表达式中的空格,需要要忽略
      */
     public final static char BLANK = ' ';

     /**
      * 小数点符号
      */
     public final static char DECIMAL_POINT = '.';

     /**
      * 负号
      */
     public final static char NEGATIVE_SIGN = '-';

     /**
      * 正号
      */
     public final static char POSITIVE_SIGN = '+';

     /**
      * 后缀表达式的各段的分隔符
      */
     public final static char SEPARATOR = ' ';

     /**
      * 解析并计算表达式
      *
      * @param expression
      * @return
      */
     public BigDecimal eval(String expression) {
         String str = infix2Suffix(expression);
         logger.info("Infix Expression: " + expression);
         logger.info("Suffix Expression: " + str);
         if (str == null) {
             throw new IllegalArgumentException("Infix Expression is null!");
         }
         return evalInfix(str);
     }

     /**
      * 对后缀表达式进行计算
      *
      * @param expression
      * @return
      */
     private BigDecimal evalInfix(String expression) {
         String[] strs = expression.split("\\s+");
         Stack<String> stack = new Stack<String>();
         for (int i = 0; i < strs.length; i++) {
             if (!Operator.isOperator(strs[i])) {
                 stack.push(strs[i]);
             } else {
                 Operator op = Operator.getInstance(strs[i]);
                 BigDecimal right =new BigDecimal(stack.pop());
                 BigDecimal left =new BigDecimal(stack.pop());
                 BigDecimal result = op.eval(left, right);
                 stack.push(String.valueOf(result));
             }
         }
         return new BigDecimal(stack.pop());
     }

     /**
      * 将中缀表达式转换为后缀表达式<br>
      * 具体算法规则 81      * 1)计算机实现转换: 将中缀表达式转换为后缀表达式的算法思想:
      *     开始扫描;
      *         数字时,加入后缀表达式;
      *         运算符:
      *  a.若为 '(',入栈;
      *  b.若为 ')',则依次把栈中的的运算符加入后缀表达式中,直到出现'(',从栈中删除'(' ;
      *  c.若为 除括号外的其他运算符 ,当其优先级高于栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止。
      *  ・当扫描的中缀表达式结束时,栈中的的所有运算符出栈; 
      *
      * @param expression
      * @return
      */
     public String infix2Suffix(String expression) {
         if (expression == null) return null;

         Stack<Character> stack = new Stack<Character>();

         char[] chs = expression.toCharArray();
         StringBuilder sb = new StringBuilder(chs.length);

         boolean appendSeparator = false;
         boolean sign = true;
         for (int i = 0; i < chs.length; i++) {
             char c = chs[i];

             // 空白则跳过
             if (c == BLANK)continue;

             // Next line is used output stack information.
             // System.out.printf("%-20s %s%n", stack, sb.toString());

             // 添加后缀表达式分隔符
             if (appendSeparator) {
                 sb.append(SEPARATOR);
                 appendSeparator = false;
             }

             if (isSign(c) && sign) {
                 sb.append(c);
             } else if (isNumber(c)) {
                 sign = false;// 数字后面不是正号或负号,而是操作符+-
                 sb.append(c);
             } else if (isLeftBracket(c)) {
                 stack.push(c);
             } else if (isRightBracket(c)) {
                 sign = false;

                 // 如果为),则弹出(上面的所有操作符,并添加到后缀表达式中,并弹出(
                 while (stack.peek() != LEFT_BRACKET) {
                     sb.append(SEPARATOR).append(stack.pop());
                 }
                 stack.pop();
             } else {
                 appendSeparator = true;
                 if (Operator.isOperator(c)) {
                     sign = true;

                     // 若为(则入栈
                     if (stack.isEmpty() || stack.peek() == LEFT_BRACKET) {
                         stack.push(c);
                         continue;
                     }
                     int precedence = Operator.getPrority(c);
                     while (!stack.isEmpty() && Operator.getPrority(stack.peek()) >= precedence) {
                         sb.append(SEPARATOR).append(stack.pop());
                     }
                     stack.push(c);
                 }
             }
         }
         while (!stack.isEmpty()) {
             sb.append(SEPARATOR).append(stack.pop());
         }
         return sb.toString();
     }

     /**
      * 判断某个字符是否是正号或者负号
      *
      * @param c
      * @return
      */
     private boolean isSign(char c) {
         return (c == NEGATIVE_SIGN || c == POSITIVE_SIGN);
     }

     /**
      * 判断某个字符是否为数字或者小数点
      *
      * @param c
      * @return
      */
     private boolean isNumber(char c) {
         return ((c >= '0' && c <= '9') || c == DECIMAL_POINT);
     }

     /**
      * 判断某个字符是否为左括号
      *
      * @param c
      * @return
      */
     private boolean isLeftBracket(char c) {
         return c == LEFT_BRACKET;
     }

     /**
      * 判断某个字符是否为右括号
      *
      * @param c
      * @return
      */
     private boolean isRightBracket(char c) {
         return c == RIGHT_BRACKET;
     }

最后把操作符类贴上
复制代码 代码如下:
View Code
 public abstract class Operator {
     /**
      * 运算符
      */
     private char operator;

     /**
      * 运算符的优先级别,数字越大,优先级别越高
      */
     private int priority;

     private static Map<Character, Operator> operators = new HashMap<Character, Operator>();

     private Operator(char operator, int priority) {
         setOperator(operator);
         setPriority(priority);
         register(this);
     }

     private void register(Operator operator) {
         operators.put(operator.getOperator(), operator);
     }

     /**
      * 加法运算
      */
     public final static Operator ADITION = new Operator('+', 100) {
         public BigDecimal eval(BigDecimal left, BigDecimal right) {
             return left.add(right);
         }
     };

     /**
      * 减法运算
      */
     public final static Operator SUBTRATION = new Operator('-', 100) {
         public BigDecimal eval(BigDecimal left, BigDecimal right) {
             return left.subtract(right);
         }
     };

     /**
      * 乘法运算
      */
     public final static Operator MULTIPLICATION = new Operator('*', 200) {
         public BigDecimal eval(BigDecimal left, BigDecimal right) {
             return left.multiply(right);
         }
     };

     /**
      * 除法运算
      */
     public final static Operator DIVITION = new Operator('/', 200) {
         public BigDecimal eval(BigDecimal left, BigDecimal right) {
             return left.divide(right);
         }
     };

     /**
      * 缭怂
      */
     public final static Operator EXPONENT = new Operator('^', 300) {
         public BigDecimal eval(BigDecimal left, BigDecimal right) {
             return left.pow(right.intValue());
         }
     };

     public char getOperator() {
         return operator;
     }

     private void setOperator(char operator) {
         this.operator = operator;
     }

     public int getPriority() {
         return priority;
     }

     private void setPriority(int priority) {
         this.priority = priority;
     }

     /**
      * 根据某个运算符获得该运算符的优先级别
      *
      * @param c
      * @return 运算符的优先级别
      */
     public static int getPrority(char c) {
         Operator op = operators.get(c);
         return op != null ? op.getPriority() : 0;
     }

     /**
      * 工具方法,判断某个字符是否是运算符
      *
      * @param c
      * @return 是运算符返回 true,否则返回 false
      */
     public static boolean isOperator(char c) {
         return getInstance(c) != null;
     }

     public static boolean isOperator(String str) {
         return str.length() > 1 ? false : isOperator(str.charAt(0));
     }

     /**
      * 根据运算符获得 Operator 实例
      *
      * @param c
      * @return 从注册中的 Operator 返回实例,尚未注册返回 null
      */
     public static Operator getInstance(char c) {
         return operators.get(c);
     }

     public static Operator getInstance(String str) {
         return str.length() > 1 ? null : getInstance(str.charAt(0));
     }

     /**
      * 根据操作数进行计算
      *
      * @param left
      *            左操作数
      * @param right
      *            右操作数
      * @return 计算结果
      */
     public abstract BigDecimal eval(BigDecimal left, BigDecimal right);


  • 上一条:
    MongoDB快速入门笔记(八)之MongoDB的java驱动操作代码讲解
    下一条:
    Java连接MySql的详细介绍
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在java中实现的脱敏工具类代码示例分享(0个评论)
    • zookeeper安装流程步骤(0个评论)
    • 在java中你背的“八股文”可能已经过时了(2个评论)
    • 在php8.0+版本中使用属性来增加值代码示例(3个评论)
    • java 正则表达式基础,实例学习资料收集大全 原创(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-11
    • 2018-03
    • 2020-03
    • 2023-05
    • 2023-11
    • 2024-01
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客