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

(编辑:jimmy 日期: 2024/12/28 浏览:2)

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

  显然定义的公式都是以字符串来存储到数据库的,可是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);

一句话新闻

微软与英特尔等合作伙伴联合定义“AI PC”:键盘需配有Copilot物理按键
几个月来,英特尔、微软、AMD和其它厂商都在共同推动“AI PC”的想法,朝着更多的AI功能迈进。在近日,英特尔在台北举行的开发者活动中,也宣布了关于AI PC加速计划、新的PC开发者计划和独立硬件供应商计划。
在此次发布会上,英特尔还发布了全新的全新的酷睿Ultra Meteor Lake NUC开发套件,以及联合微软等合作伙伴联合定义“AI PC”的定义标准。