计算引擎--扩展篇
一、前言
我之前写过一篇计算引擎分享文章(简单计算引擎分享),受限于计算场景,文章内只实现了“双元组”公式计算,本篇文章将继续对其计算方法进行改进,实现支持“多元组”公式的计算。
二、限定场景
与简单计算引擎分享一样,我们也先来确定好支持场景:
- 1.设定计算公式仅支持加减乘除法的计算(不变)
- 2.公式内分隔符采用中括号分割"()",其他符号不行,且必须成对儿出现(由"[]"改为"()")
- 3.支持多元组计算(由二元组计算扩展为多元组)
- 4.计算的最小因子,只支持正数和零,暂不支持负数(不变)
可以看到,相比于简单计算引擎分享,我们这里将“二元组”扩展为了“多元组”计算,具体实现看后面代码实现。
三、代码实现
首先,先来看一下我们构建好的项目结构:
1.实体类
两个实体对象,Constant:常量符号类;Operator:计算符号枚举类。
/**
* Title 常量
*
* @author Ason(18078490)
* @date 2020-09-27
*/
public final class Constant {
public static final char COMMA = ',';
public static final char LEFT_BRACKET = '(';
public static final char RIGHT_BRACKET = ')';
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Arrays;
/**
* Title 计算符号
* 目前,仅支持:加减乘除
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@AllArgsConstructor
public enum Operator {
// 加
ADD('+', 2),
// 减
SUBTRACT('-', 2),
// 乘
MULTIPLY('*', 1),
// 除
DIVIDE('/', 1);
@Setter
@Getter
private char c;
@Setter
@Getter
private int priority;
public static Operator correspond(char c) {
return Arrays.stream(Operator.values()).filter(operator -> operator.c == c).findFirst().orElse(null);
}
public static boolean anyMatch(char c) {
return Arrays.stream(Operator.values()).anyMatch(operator -> operator.c == c);
}
}
2.计算类
针对“加减乘除”计算,我们抽象出最基本的两个元素计算接口Eval。
import java.math.BigDecimal;
/**
* Title 计算
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@FunctionalInterface
public interface Eval {
/**
* 计算方法
*
* @param first 第一个元素
* @param second 第二个元素
* @return 计算结果
*/
BigDecimal eval(String first, String second);
}
分别实现该计算接口的“加减乘除”计算:
import cn.wxson.cal.complex.biz.Eval;
import java.math.BigDecimal;
/**
* Title 加法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Add implements Eval {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.add(secondBd);
}
}
import cn.wxson.cal.complex.biz.Eval;
import java.math.BigDecimal;
/**
* Title 减法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Subtract implements Eval {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.subtract(secondBd);
}
}
import cn.wxson.cal.complex.biz.Eval;
import java.math.BigDecimal;
/**
* Title 乘法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Multiply implements Eval {
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.multiply(secondBd);
}
}
import cn.wxson.cal.complex.biz.Eval;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* Title 除法
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Divide implements Eval {
private static final int DEFAULT_SCALE = 2;
@Override
public BigDecimal eval(String first, String second) {
BigDecimal firstBd = new BigDecimal(first);
BigDecimal secondBd = new BigDecimal(second);
return firstBd.divide(secondBd, DEFAULT_SCALE, RoundingMode.HALF_UP);
}
}
3.计算工厂
采用“工厂模式”根据计算符来选定具体的计算逻辑:
import cn.wxson.cal.complex.biz.Eval;
import cn.wxson.cal.complex.biz.impl.Add;
import cn.wxson.cal.complex.biz.impl.Divide;
import cn.wxson.cal.complex.biz.impl.Multiply;
import cn.wxson.cal.complex.biz.impl.Subtract;
import cn.wxson.cal.complex.model.Operator;
import lombok.extern.slf4j.Slf4j;
/**
* Title 算子工厂
*
* @author Ason(18078490)
* @date 2020-07-24
*/
@Slf4j
public class EvalFactory {
/**
* 根据计算符获得计算实例
*
* @param operator 计算符
* @return 计算实例
*/
public static Eval create(Operator operator) {
switch (operator) {
case ADD:
return new Add();
case SUBTRACT:
return new Subtract();
case MULTIPLY:
return new Multiply();
case DIVIDE:
return new Divide();
default:
log.error("[不存在该计算符:{}]", operator);
throw new NullPointerException("[不存在的计算符]");
}
}
}
4.计算类
我们采用“后缀式栈”的方式实现“多元组计算”中的公式处理逻辑。代码中已添加必要步骤说明,具体如下:
import cn.wxson.cal.complex.factory.EvalFactory;
import cn.wxson.cal.complex.model.Constant;
import cn.wxson.cal.complex.model.Operator;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.Stack;
/**
* Title 计算工具类
*
* @author Ason(18078490)
* @date 2020-09-27
*/
public class Calculator {
/**
* 后缀式栈
*/
private final Stack<String> postfixStack = new Stack<>();
/**
* 运算符栈
*/
private final Stack<Character> operatorStack = new Stack<>();
/**
* 运用运算符ASCII码-40做索引的运算符优先级 ( + * ) ? ) ( *
*/
private final int[] operatorPriority = {0, 3, 2, 1, -1, 1, 0, 2};
/**
* 公式计算
*
* @param expression 公式
* @return 计算结果
*/
public String calculate(String expression) {
// 数据准备阶段将表达式转换成为后缀式栈
analysis(expression);
// 翻转后缀栈
Collections.reverse(postfixStack);
// 依次循环计算
Stack<String> resultStack = loopCalculate();
return resultStack.pop();
}
private Stack<String> loopCalculate() {
Stack<String> resultStack = new Stack<>();
String firstValue;
String secondValue;
String currentValue;
while (!postfixStack.isEmpty()) {
currentValue = postfixStack.pop();
if (!isOperator(currentValue.charAt(0))) {
// 如果不是运算符则存入操作数栈中
resultStack.push(currentValue);
} else {
// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
secondValue = resultStack.pop();
firstValue = resultStack.pop();
String rs = calculate(firstValue, secondValue, currentValue.charAt(0));
resultStack.push(rs);
}
}
return resultStack;
}
private void analysis(String expression) {
// 运算符放入栈底元素逗号,此符号优先级最低
operatorStack.push(Constant.COMMA);
// 当前字符的位置
int currentIndex = 0;
// 上次算术运算符到本次算术运算符的字符长度或者之间的数值
int count = 0;
// 当前操作符
char currentOp;
// 栈顶操作符
char peekOp;
String replace = StringUtils.replace(expression, " ", "");
char[] arr = replace.toCharArray();
for (int i = 0; i < arr.length; i++) {
currentOp = arr[i];
if (isOperator(currentOp)) {
if (count > 0) {
// 取两个运算符之间的数字
postfixStack.push(new String(arr, currentIndex, count));
}
peekOp = operatorStack.peek();
// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
if (currentOp == Constant.RIGHT_BRACKET) {
currentOperator();
} else {
otherOperator(currentOp, peekOp);
}
count = 0;
currentIndex = i + 1;
} else {
count++;
}
}
// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {
postfixStack.push(new String(arr, currentIndex, count));
}
while (operatorStack.peek() != Constant.COMMA) {
// 将操作符栈中的剩余的元素添加到后缀式栈中
postfixStack.push(String.valueOf(operatorStack.pop()));
}
}
private void otherOperator(char currentOp, char peekOp) {
char newPeekOp = peekOp;
while (currentOp != Constant.LEFT_BRACKET && newPeekOp != Constant.COMMA && compare(currentOp, newPeekOp)) {
postfixStack.push(String.valueOf(operatorStack.pop()));
newPeekOp = operatorStack.peek();
}
operatorStack.push(currentOp);
}
private void currentOperator() {
while (operatorStack.peek() != Constant.LEFT_BRACKET) {
postfixStack.push(String.valueOf(operatorStack.pop()));
}
operatorStack.pop();
}
private boolean isOperator(char c) {
return Operator.anyMatch(c) || c == Constant.LEFT_BRACKET || c == Constant.RIGHT_BRACKET;
}
public boolean compare(char cur, char peek) {
// 利用ASCII码-40做下标去算术符号优先级。
// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低
return operatorPriority[(peek) - 40] >= operatorPriority[(cur) - 40];
}
private String calculate(String first, String second, char operator) {
Operator symbol = Operator.correspond(operator);
Eval eval = EvalFactory.create(symbol);
return eval.eval(first, second).toString();
}
}
5.测试
import cn.wxson.cal.complex.biz.Calculator;
/**
* Title 测试类
*
* @author Ason(18078490)
* @date 2020-07-24
*/
public class Domain {
public static void main(String[] arg) {
String formula = " ( (20* 2 + 1) + (( 1- 3 ) *2) + 5 * 5) /12 -1 ";
Calculator calculator = new Calculator();
String rs = calculator.calculate(formula);
System.out.println(rs);
}
}
我们随便写了一个公式,并尽量复杂( (20* 2 + 1) + (( 1- 3 ) *2) + 5 * 5) /12 -1
,对其求值,结果:
四、文末
本文,我们对简单计算引擎分享
进行了多元组
的扩展,实现方法采用了后缀式栈
的方法,虽然计算支持的场景有限,但我们今后还会对他继续扩展,已支持我们更多场景需求,敬请关注!
-----------------------------------------------------------
个人网址:http://wxson.cn(待开通)
-----------------------------------------------------------
个人网址:http://wxson.cn(待开通)
-----------------------------------------------------------