简单计算引擎分享
一、背景
最近,工作中经常遇到公式计算的情况,虽然都是加减乘除的简单运算,但使用比较频繁,于是,自己就趁着业余时间手写了一个仅支持加减乘除法的计算引擎,分享出来,供大家一起学习!
首先,一遇到简单计算,可能很多人都会想到Java通过JavaScript引擎调用Javascript数学函数实现计算,创建实例如下:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
虽然这种方式,直接使用起来很简单,但引擎本身启动是比较耗资源的,而且使用JavaScript的外部计算,毕竟不符合做为一个合格码农的作风,因此,干脆自己写一个,既方便自己使用,又可以随时根据场景修改!
好了,话不多说,进入正题!
二、限定场景
动手编写前,我们首先明确一些限定场景,毕竟是简单计算,不需要兼容太多内容,所以,我们做如下设定:
- 1.设定计算公式仅支持加减乘除法的计算
- 2.公式内分隔符采用中括号分割"[]",其他符号不行,且必须成对儿出现
- 3.仅支持双元组计算。即:每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
- 4.计算的最小因子,只支持正数和零,暂不支持负数
- 支持举例:
- 2 + 3
- 2 + [ 3 + 4]
- [ 2 + 3 ] + [ 3 + 4]
- [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
- 不支持举例:
- 2 + 3 + 4 // 多于两个元素
- 2 + ( 3 + 4 ) // 分隔符必须是中括号
- 2 + [ 3 + 4 ] + 5 // 多于两个元素
三、代码实现
代码中会使用到插件lombok,请同学们自行提前安装!
1.实体类
首先我们定义好需要操作的实体对象。这里有三个,Constant:常量符号类;Symbol:计算符号枚举类;Operator:计算公式类。
package cn.wxson.cal.model;
/**
* Title 常量类
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public interface Constant {
String BRACKETS_START = "[";
String BRACKETS_END = "]";
String SPECIAL = "###";
}
package cn.wxson.cal.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Title 计算符号
* 目前,仅支持:加减乘除
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@AllArgsConstructor
public enum Symbol {
ADD("+", 2, "加"),
SUBTRACT("-", 2, "减"),
MULTIPLY("*", 1, "乘"),
DIVIDE("/", 1, "除");
/**
* 计算符字符串
*/
@Setter
@Getter
private String literal;
/**
* 计算符优先级
*/
@Setter
@Getter
private int priority;
/**
* 计算符名称
*/
private String name;
}
package cn.wxson.cal.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* Title 公式
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Setter
@Getter
@Builder
public class Operator {
/**
* 第一位表达式
*/
private String first;
/**
* 第二位表达式
*/
private String second;
/**
* 表达式间的计算符
*/
private Symbol symbol;
}
2.计算类
因为我们要做加减乘除计算,所以可以根据两个计算因子来定义好计算接口Calculate。
package cn.wxson.cal.biz;
import java.math.BigDecimal;
/**
* Title 计算
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@FunctionalInterface
public interface Calculate {
/**
* 计算方法
*
* @param first 第一个元素
* @param second 第二个元素
* @return 计算结果
*/
BigDecimal eval(String first, String second);
}
在Calculate的基础上,我们来实现加法、减法、乘法、除法的运算操作。
package cn.wxson.cal.biz.impl;
import cn.wxson.cal.biz.Calculate;
import java.math.BigDecimal;
/**
* Title 加法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Add implements Calculate {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.add(secondBd);
}
}
package cn.wxson.cal.biz.impl;
import cn.wxson.cal.biz.Calculate;
import java.math.BigDecimal;
/**
* Title 减法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Subtract implements Calculate {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.subtract(secondBd);
}
}
package cn.wxson.cal.biz.impl;
import cn.wxson.cal.biz.Calculate;
import java.math.BigDecimal;
/**
* Title 乘法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Multiply implements Calculate {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.multiply(secondBd);
}
}
package cn.wxson.cal.biz.impl;
import cn.wxson.cal.biz.Calculate;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* Title 除法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Divide implements Calculate {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.divide(secondBd, 2, RoundingMode.HALF_UP);
}
}
最后,计算类创建成功后,我们还需要一个计算工厂类,来根据计算符号创建具体的计算实例。
package cn.wxson.cal.factory;
import cn.wxson.cal.biz.Calculate;
import cn.wxson.cal.biz.impl.Add;
import cn.wxson.cal.biz.impl.Divide;
import cn.wxson.cal.biz.impl.Multiply;
import cn.wxson.cal.biz.impl.Subtract;
import cn.wxson.cal.model.Symbol;
import lombok.extern.slf4j.Slf4j;
/**
* Title 算子工厂
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Slf4j
public class OperatorFactory {
/**
* 根据计算符获得计算实例
*
* @param symbol 计算符
* @return 计算实例
*/
public static Calculate createCal(Symbol symbol) {
switch (symbol) {
case ADD:
return new Add();
case SUBTRACT:
return new Subtract();
case MULTIPLY:
return new Multiply();
case DIVIDE:
return new Divide();
default:
log.error("[不存在该计算符] [计算符:{}]", symbol);
throw new NullPointerException("[不存在的计算符]");
}
}
}
3.工具类
在正式进行计算公式解析前,我们来创建两个工具类,辅助我们后续操作,分别是字符操作工具:StringUtil,计算辅助工具:FormulaUtil。
package cn.wxson.cal.util;
import cn.wxson.cal.model.Constant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Stack;
/**
* Title 字符串操作
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Slf4j
public final class StringUtil {
/**
* 分隔符
*/
public static final String[] SRC_BRACKETS = {Constant.BRACKETS_START, Constant.BRACKETS_END};
/**
* 清洗分隔符为空的集合
*/
public static final String[] DESC_BRACKETS = {StringUtils.EMPTY, StringUtils.EMPTY};
/**
* 字符:[
*/
public static final char SC = Constant.BRACKETS_START.toCharArray()[0];
/**
* 字符:]
*/
public static final char EC = Constant.BRACKETS_END.toCharArray()[0];
/**
* 从开始分隔符下标开始,获取与其对应的结束分隔符索引
* 这里使用stack结构,便于标识多重分隔符号场景下,找到目标结束符
*
* @param formula 公式
* @param index 开始分割符的下标,即"["的下标
* @return 与开始分隔符"["相对应的结束分隔符"]"的下标
*/
public static int index(String formula, int index) {
Stack<Integer> stack = new Stack<>();
char[] chars = formula.toCharArray();
for (int i = index + 1; i < chars.length; i++) {
char c = chars[i];
if (c == SC) {
stack.push(i);
} else if (c == EC) {
if (stack.empty()) {
return i;
} else {
stack.pop();
}
}
}
return -1;
}
/**
* 清洗字符串
*
* @param value 字符串值
* @return 清洗结果
*/
public static String clean(String value) {
return trim(replace(value));
}
/**
* 去除字符串两端空字符
*
* @param value 字符串值
* @return 结果
*/
public static String trim(String value) {
return StringUtils.trim(value);
}
/**
* 替换字符串中的分隔符为空
*
* @param value 字符串值
* @return 替换结果
*/
public static String replace(String value) {
return StringUtils.replaceEach(value, SRC_BRACKETS, DESC_BRACKETS);
}
}
package cn.wxson.cal.util;
import cn.wxson.cal.model.Symbol;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Optional;
/**
* Title 计算辅助工具
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Slf4j
public final class FormulaUtil {
/**
* 根据计算符字符串,获取计算符对象
*
* @param formula 公式
* @return 计算符对象
*/
public static Symbol symbol(String formula) {
Optional<Symbol> first = Arrays.stream(Symbol.values()).filter(symbol -> StringUtils.contains(formula, symbol.getLiteral())).findFirst();
if (!first.isPresent()) {
log.error("[公式内不包含任何计算符] [公式:{}]", formula);
throw new NullPointerException("[公式内不包含任何计算符]");
}
return first.get();
}
/**
* 根据计算公式,判断是否只有一个计算符
*
* @param formula 公式
* @return 判断结果
*/
public static boolean isSingleSymbol(String formula) {
return countSymbol(formula) == 1;
}
/**
* 根据计算公式,获取计算符个数
*
* @param formula 公式
* @return 个数
*/
public static int countSymbol(String formula) {
return Arrays.stream(Symbol.values()).map(symbol -> StringUtils.countMatches(formula, symbol.getLiteral())).reduce(Integer::sum).orElse(0);
}
/**
* 根据计算公式,判断是否存在计算符
*
* @param formula 公式
* @return 判断结果
*/
public static boolean noSymbol(String formula) {
Optional<Symbol> any = Arrays.stream(Symbol.values()).filter(symbol -> StringUtils.contains(formula, symbol.getLiteral())).findAny();
return !any.isPresent();
}
}
4.解析类
最后一步,我们根据计算公式来解析出最后的计算结果,结果我们定义为BigDecimal,计算公式以字符串形式传入。解析接口如下:
package cn.wxson.cal.biz;
import java.math.BigDecimal;
/**
* Title 解析
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@FunctionalInterface
public interface Parser {
/**
* 该解析方法,适用场景有如下限制:
* 1.公式目前仅支持加减乘除法计算
* 2.公式内分隔符采用"[]",其他符号不行,且必须成对儿出现
* 3.每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
* 4.计算最小因子,只支持正数和零,暂不支持负数
* 支持举例:
* 2 + 3
* 2 + [ 3 + 4]
* [ 2 + 3 ] + [ 3 + 4]
* [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
* 不支持举例:
* 2 + 3 + 4
* 2 + [ 3 + 4 ] + 5
* 2 + ( 3 + 4 )
*
* @param formula 公式,例如:[ 2 + 3 ] - 1 或 [ 2 + 3 ] - [1 + 4]
* @return 解析后的计算值
*/
BigDecimal parse(String formula);
}
具体的解析实例会稍微复杂点儿,其中会用到两次递归操作,不过,对于两三年编程经验的人来说,就是小菜一碟儿了。
package cn.wxson.cal.biz.impl;
import cn.wxson.cal.biz.Calculate;
import cn.wxson.cal.biz.Parser;
import cn.wxson.cal.factory.OperatorFactory;
import cn.wxson.cal.model.Constant;
import cn.wxson.cal.model.Operator;
import cn.wxson.cal.model.Symbol;
import cn.wxson.cal.util.FormulaUtil;
import cn.wxson.cal.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
/**
* Title 公式解析
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Slf4j
public class ParserImpl implements Parser {
/**
* 该解析方法,适用场景有如下限制:
* 1.公式目前仅支持加减乘除法计算
* 2.公式内分隔符采用"[]",其他符号不行,且必须成对儿出现
* 3.每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
* 4.计算最小因子,只支持正数和零,暂不支持负数
* 支持举例:
* 2 + 3
* 2 + [ 3 + 4]
* [ 2 + 3 ] + [ 3 + 4]
* [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
* 不支持举例:
* 2 + 3 + 4
* 2 + [ 3 + 4 ] + 5
* 2 + ( 3 + 4 )
*
* @param formula 公式,例如:[ 2 + 3 ] - 1 或 [ 2 + 3 ] - [1 + 4]
* @return 解析后的计算值
*/
@Override
public BigDecimal parse(String formula) {
// 1.不包含计算符号时,为单数字
if (FormulaUtil.noSymbol(formula)) {
String value = StringUtil.clean(formula);
return new BigDecimal(value);
}
// 2.单符号计算时,为直接表达式,例如:2 + 1
if (FormulaUtil.isSingleSymbol(formula)) {
String value = StringUtil.clean(formula);
return directEval(value);
}
// 3.多符号计算情况下,前后都是间接表达式,例如:[ 2 + 1 ] + 1 或 [ 2 + 1] + [ 2 + 1 ]
Operator operator = parseTo(formula);
BigDecimal first = parse(operator.getFirst());
BigDecimal second = parse(operator.getSecond());
Calculate cal = OperatorFactory.createCal(operator.getSymbol());
return cal.eval(first.toString(), second.toString());
}
/**
* 对多符号计算公式进行解析
*
* @param formula 公式,例如:1 + [ 2 + 3] 或 [ 1 + 2 ] + [ 2 + 3]
* @return 解析结果
*/
private Operator parseTo(String formula) {
int startIndex = StringUtils.indexOf(formula, Constant.BRACKETS_START);
if (startIndex == -1) {
log.error("[多符号计算情况下,不存在分隔符\"[]\"] [表达式:{}]", formula);
throw new RuntimeException("[多符号计算情况下,不存在分隔符\"[]\"]");
}
int endIndex = StringUtil.index(formula, startIndex);
String first = StringUtils.substring(formula, startIndex + 1, endIndex);
String replace = StringUtils.replace(formula, Constant.BRACKETS_START + first + Constant.BRACKETS_END, Constant.SPECIAL);
int startIndex2 = StringUtils.indexOf(replace, Constant.BRACKETS_START);
if (startIndex2 == -1) {
Symbol symbol = FormulaUtil.symbol(replace);
String second = StringUtils.replaceEach(replace, new String[]{Constant.SPECIAL, symbol.getLiteral()}, new String[]{StringUtils.EMPTY, StringUtils.EMPTY});
return Operator.builder().first(first).second(second).symbol(symbol).build();
}
int endIndex2 = StringUtil.index(replace, startIndex2);
String second = StringUtils.substring(replace, startIndex2 + 1, endIndex2);
String replace2 = StringUtils.replace(replace, Constant.BRACKETS_START + second + Constant.BRACKETS_END, Constant.SPECIAL);
Symbol symbol = FormulaUtil.symbol(replace2);
return Operator.builder().first(first).second(second).symbol(symbol).build();
}
/**
* 对单符号公式进行直接计算
*
* @param formula 公式
* @return 计算结果
*/
public static BigDecimal directEval(String formula) {
Symbol symbol = FormulaUtil.symbol(formula);
int index = StringUtils.indexOf(formula, symbol.getLiteral());
String first = StringUtils.substring(formula, 0, index);
String second = StringUtils.substring(formula, index + 1);
Calculate cal = OperatorFactory.createCal(symbol);
return cal.eval(first, second);
}
}
5.测试
最后代码编写完毕,我们来创建个测试类来实验下效果。
package cn.wxson.cal.test;
import cn.wxson.cal.biz.Parser;
import cn.wxson.cal.biz.impl.ParserImpl;
import java.math.BigDecimal;
/**
* Title 测试类
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Domain {
public static void main(String[] arg) {
String formula = " [[20*2]+[[1-3] *2] ] / 4 ";
Parser parser = new ParserImpl();
BigDecimal result = parser.parse(formula);
System.out.println(result.toString());
}
}
通过上面的测试类,我们可以发现,计算公式" [[20*2]+[[1-3] *2] ] / 4 "故意被我写的复杂些,其中有多层嵌套,而且包含了加减乘除各种运算,还有空格等,我们来看计算结果:
怎么样?是不是你期望的答案?😊
四、总结
通过这个简单计算引擎的编写,我们可以发现,其实这种编程并不难,其中比较复杂的部分就是公式解析功能,只要提前设定好你的引擎使用场景,相信也不会难到你!
另外,对于复杂计算引擎的考虑,目前还在预想中,今后做好,也会分享给大家,敬请期待!
😄
个人网址:http://wxson.cn(待开通)
-----------------------------------------------------------