使用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 values = new HashMap();

values.put("dddd", BigDecimal.valueOf(56d));

//需要依赖的其他公式

Map formulas = new HashMap();

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 formulas, Map 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 values) {

if (values == null)values = Collections.emptyMap();

return parse(formula, Collections. emptyMap(), values);

}

/**

* 解析公式,并执行公式计算

*

* @param formula

* @return

*/

public static BigDecimal parse(String formula) {

return parse(formula, Collections. emptyMap(), Collections. emptyMap());

}

/**

* 将所有中间变量都替换成基础数据

*

* @param expression

* @param formulas

* @param values

* @return

*/

private static String finalExpression(String expression, Map formulas, Map 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 stack = new Stack();

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());

}

/**

* 将中缀表达式转换为后缀表达式

* 具体算法规则 81      * 1)计算机实现转换: 将中缀表达式转换为后缀表达式的算法思想:

*     开始扫描;

*         数字时,加入后缀表达式;

*         运算符:

*  a.若为 '(',入栈;

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

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

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

*

* @param expression

* @return

*/

public String infix2Suffix(String expression) {

if (expression == null) return null;

Stack stack = new Stack();

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 operators = new HashMap();

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);

相关阅读:

C语言实现输入一个字符串后打印出该字符串中字符的所有排列

win7系统安全无损调整分区大小的详细教程

Android应用中使用ViewPager和ViewPager指示器来制作Tab标签

Win7电脑菜单中的图标拖不到桌面怎么办 两种解决方法

sql 截取域名的问题

Android学习小结之Activity保存和恢复状态

PHP查找与搜索数组元素方法总结

JavaScript对象之深度克隆介绍

php实现下载限制速度示例分享

java中break和continue源码解析

Oracle 11g服务器与客户端卸载、安装全过程

Win7精简版下安装针式打印机驱动程序的教程

JS实现跟随鼠标立体翻转图片的方法

Json_encode防止汉字转义成unicode的方法

php如何让字符串变运算公式,MySQL_使用java处理字符串公式运算的方法,  在改进一个关于合同的项 - phpStudy...相关推荐

  1. 判断字符串不包含某个字符php,java判断字符串是否包含某个字符的方法

    java判断字符串是否包含某个字符的方法: 一.contains方法 1:描述 java.lang.String.contains() 方法返回true,当且仅当此字符串包含指定的char值序列 2: ...

  2. java 字符串截取的几种方式 java获取当前路径的几种方法

    java 字符串截取的几种方式: https://blog.csdn.net/qq_27603235/article/details/51604584 java获取当前路径的几种方法: https:/ ...

  3. java移位运算 cpu gpu_关于java操作中的移位运算

    packagecom.dgjianke.ch03;/*** 关于二进制数据的一些操作 *@authordgjianke **/ public classBitManipulation {/*** 打印 ...

  4. php判断是为字符串类型,PHP:检查变量是否为字符串类型并且不是空字符串?

    我需要检查传递的变量是否是字符串的类型,并且它不是空的.我有以下功能: function isNonEmptyStr($var) { if(isset($var)) { if(is_string($v ...

  5. java反转字符串的方法

    1.首先我们定义一个方法,用来反转字符串.用 public static void (String)方法初始化一个对象,然后使用 private static ()方法对该对象进行初始化,并检查是否有 ...

  6. java 比较字符串前几位_java截取字符串前几位

    java截取字符串_IT/计算机_专业资料.java截取字符串 1根据字符串 S... java中常用的字符串的截取方法_计算机软件及应用_IT/计算机_专业资料.jsp中常见使用技术,js,jstl ...

  7. java 字符串格式替换_[Java] - 格式字符串替换方法

    Java 字符串格式替换方法有两种,一种是使用String.format(...),另一种是使用MessageFormat.format(...) 如下: import java.text.Messa ...

  8. java split函数的用法,java拆分字符串_java中split拆分字符串函数用法

    摘要 腾兴网为您分享:java中split拆分字符串函数用法,中信期货,掌上电力,星球联盟,淘集集等软件知识,以及韩剧精灵,每日英语听力vip,龙卷风收音机,优衣库,中国平煤神马集团协同办公系统,光晕 ...

  9. 叉乘点乘混合运算公式_初中数学学不会?公式这样记,让你做题效率翻倍!

    中学大课堂初中各科学习必备(海量学习资源) 今天给大家分享初中数学公式记忆的一些小技巧,学会了,做题效率肯定会有提升! 01 有理数的加法 同号相加一边倒;异号相加"大"减&quo ...

最新文章

  1. 样条之连分式插值函数
  2. SSH远程联机Linux服务器简易安全设定
  3. wampserver2.5安装 redis缓存,igbinary, phalcon框架
  4. Nacos client SDK 订阅式请求坑
  5. Google 出的 Guava 是个什么鬼
  6. C#中的变量类型(值类型、引用类型)
  7. oracle 转 mysql 乱码问题吗_Oracle数据传输MySQL中文编码问题
  8. mysql去重查询group_MySQL distinct 与 group by 去重(where/having)
  9. LeetCode Number of Digit One
  10. 1.1 c和c++关系
  11. 使用lkjson处理Json数据
  12. GNS 3路由器7200介绍
  13. Django ORM 使用手册
  14. hdu 5652 India and China Origins 并查集
  15. win10清除系统垃圾的几个命令
  16. win7系统还原点来还原系统
  17. 彻底卸载JDK的-并只依赖配置环境安装JDK(不依赖注册表)-解决Error opening registry key‘software\Javasoft\Java Runti问题
  18. 情侣睡觉为什么在床头总会放一包纸巾?今天终于知道了
  19. AndroidStudio学习3 显示百度地图和实现定位功能
  20. java面试技术准备

热门文章

  1. 【互联网生存法则】互联网打工人的一天
  2. URL(统一资源定位符)网络地址格式
  3. Python 中np.prod函数详解
  4. 笔记-项目采购管理-竞争性谈判采购和单一来源采购
  5. 分布式机器学习常用数据集
  6. Android中Yahoo天气预报API的使用
  7. window location 之应用
  8. simulink中的FFT 小白入门
  9. 西门子串口通讯03-CP341在Step7环境做Modbus主站通讯
  10. SnackBar 简单使用