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

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

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

(0)

相关推荐

  • java拼接字符串时去掉最后一个多余逗号的方法

    本文实例讲述了java拼接字符串时去掉最后一个多余逗号的方法.分享给大家供大家参考.具体分析如下: 先看下面这段代码: for (int t = 0; t < memberLen; t++) { memTemp = stafferMap.get(strMember[t]); if(memTemp != null){ memberNames += memTemp + ","; } } 以上的代码,拼接的字符串会多一个",",比如:"str1,str2,

  • Java实现字符串倒序输出的常用方法小结

    本文实例汇总了Java实现字符串倒序输出的常用方法,分享给大家供大家参考.具体方法如下: 1. 最容易想到的估计就是利用String类的toCharArray(),再倒序输出数组的方法了. 实现代码如下: import javax.swing.JOptionPane; public class ReverseString { public static void main (String args[]){ String originalString; String resultString =

  • java求数组元素重复次数和java字符串比较大小示例

    复制代码 代码如下: /** * Name: 求数组中元素重复次数对多的数和重复次数 * Description:  * 数组中的元素可能会重复,这个方法可以找出重复次数最多的数,同时可以返回重复了多少次. * 但需要知道这个数组中最大的元素是多少,如果无法确定,就悲剧啦~ * * @param array目标数组: *           max数组中数据的最大值: * @return 返回一个包含重复次数最多的数(value)和重复次数(maxCount)的map集合: *         

  • Java 替换字符串中的回车换行符的方法

    使用正则表达式进行替换: 代码片段: String documentTxt = EntityUtils.toString(entity,"gbk");//获取数据 documentTxt=documentTxt.replaceAll("[\\t\\n\\r]", "");//将内容区域的回车换行去除 说明:String类的replaceAll就有正则替换功能. \t为制表符 \n为换行 \r为回车 java正则使用: 示例方法: 复制代码 代码如

  • Java判断字符串中是否包含中文方法

    今天和同事在讨论一个问题,需要检查"输入的字符串中是否包含中文",刚开始想到是用正则表达式,正则表达式中是以[u4e00-u9fa5]来全匹配字符是否是中文,但现在面临的问题是这个字符串中还可能包含英文字符.数字.特殊字符,一时也没想出能匹配该场景的正则表达式,后来在网上搜了下,可以使用Matcher类来解决该问题,大致的代码实现如下: import java.util.regex.Matcher; import java.util.regex.Pattern; public clas

  • java 字符串相减(很简单的一个方法)

    有个比较简单的方法: 复制代码 代码如下: str1="abcd";str2="cd";str3=str1.replaceAll(str2,"");//str3="ab"

  • Java中去除字符串中所有空格的几种方法

    JAVA中去掉空格 1. String.trim() trim()是去掉首尾空格 2.str.replace(" ", ""); 去掉所有空格,包括首尾.中间  复制代码 代码如下: String str = " hell o ";  String str2 = str.replaceAll(" ", "");  System.out.println(str2); 3.或者replaceAll("

  • Java中char数组(字符数组)与字符串String类型的转换方法

    本文实例讲述了Java中char数组(字符数组)与字符串String类型的转换方法.分享给大家供大家参考,具体如下: 在Java语言编程时,使用"口令字段"jPasswordField组件时,如果要获得密码值,就需要使用该组件的getPassword()方法.jPasswordField的getPassword()方法返回一个char类型的数组,我们经常需要将这个数组转换为String类型,以便进行诸如口令匹配或口令赋值等操作.这时,就需要将char类型的数组进行转换.当然也经常会遇到

  • java字符串比较获取字符串出现次数的示例

    比如:javascriptjavasejavaeejavame 思路:定义一个计数器获取java第一次出现的位置从第一次出现位置后剩余的字符串中继续获取java出现的位置每获取一次就计数一次当获取不到时,计数完成 复制代码 代码如下: class StringCount{    public static void main(String[] args){        String s = "javascriptjavasejavaeejavame";        int coun

  • java中常用的字符串的比较方法(两种)

    比较字符串比较常用的两个方法是运算符"="和String的equals方法. 使用"="比较两个字符串,是比较两个对象的的"地址"是否一致,本质就是判断两个变量是否指向同一个对象,如果是则返回true,否则返回的是false.而String类的equals方法则是比较两个字符串的内容是否一致,返回值也是一个布尔类型. 看下面的代码: public class TestString{ public static void main(String[]

随机推荐