從零構建一個支援擴充套件的二目計算器
從零構建一個支援擴充套件的二目計算器
你需要了解的前置知識
- 逆波蘭字尾表示式
- 面向物件思想 -> 多型
- 快速冪 & 矩陣快速冪 ---擴充套件運算子時使用
構建計算規則
先思考計算時候我們需要注意的事項
- 運算子優先順序
- 運算子在遇到數字之後的計算規則
考慮這兩項,如果將規則直接寫到具體的計算程式碼中就會導致程式碼無法擴充套件,而這洽洽違背了面向物件設計,綜合考慮,新增一個規則介面。
package core; import java.math.BigDecimal; import java.security.InvalidParameterException; /** * 計算規則介面 */ public interface CalculatorRule { /** * 計算介面 * @param val1 雙目表示式左值 * @param val2 雙目表示式右值 * @return 計算結果 * @throws InvalidParameterException 規定引數無法解析時,丟擲該異常 */ BigDecimal method(String val1, String val2) throws InvalidParameterException; /** * 獲取該運算子的優先順序 * @return 優先順序 */ int priority(); }
構建基礎計算器
基礎的計算機中包含以下功能
- 新增運算子規則集合,若已存在該運算子則覆蓋原來的規則 -> addOrReplaceAllCalculatorRule
- 新增運算子規則集合,若已存在該運算子則跳過 -> addAllCalculatorRule
- 新增單個運算子規則,若已存在該運算子則覆蓋原來的規則 ->addOrReplaceCalculatorRule
- 新增單個運算子規則,若已存在該運算子則跳過 -> addCalculatorRule
- 獲取運算子優先順序 -> getPriority
- 獲取逆波蘭字尾表示式 -> reversePolishNotation
- 獲取表示式計算結果 -> getResult
- 計算規則 -> calculation
- 逆波蘭字尾表示式轉換核心 -> manageOperator
- 分解原始表示式 -> decompositionFormula
- 判斷字串是否是當前所支援的數字 -> isSupportNumber
- 判斷字串是否是當前所支援的運算子 -> isSupportOperator
package core; import java.math.BigDecimal; import java.math.RoundingMode; import java.security.InvalidParameterException; import java.util.*; /** * 支援雙目運算規則的基礎計算類 */ public class Calculators { Map<String, CalculatorRule> rule = new HashMap<>(); /** * 無參建構函式 */ public Calculators(){ } /** * 在初始化時,新增計算規則 * @param rule 規則對映 */ public Calculators(Map<String, CalculatorRule> rule){ this(); this.rule.putAll(rule); } /** * 新增一個規則Map, 其中Key為定義的運算子, value為定義的介面 * @param rules 規則對映表 */ public void addOrReplaceAllCalculatorRule(Map<String, ? extends CalculatorRule> rules){ this.rule.putAll(rules); } /** * 新增一個規則Map, Key為定義的運算子, value為定義的介面 * @param rules 規則對映表 */ public void addAllCalculatorRule(Map<String, ? extends CalculatorRule> rules){ Set<String> keySet = rules.keySet(); for (String item : keySet) { assert rules.get(item) != null; if (this.rule.containsKey(item)){ rules.remove(item); keySet.remove(item); } } this.rule.putAll(rules); } /** * 新增一個規則Map, Key為定義的運算子, value為定義的介面 * @param operator 操作符 * @param rule 規則對映表 */ public void addCalculatorRule(String operator, CalculatorRule rule){ if (this.rule.containsKey(operator)){ return; } this.rule.put(operator, rule); } public void addOrReplaceCalculatorRule(String operator, CalculatorRule rule){ this.rule.put(operator, rule); } /** * 獲取優先順序 * @param op1 操作符1 * @param op2 操作符2 * @return 優先順序 * @throws InvalidParameterException 在無法解析操作符時丟擲無效引數異常 */ public boolean getPriority(String op1, String op2) throws InvalidParameterException{ if (isSupportOperator(op1) && isSupportOperator(op2)){ return rule.get(op1).priority() > rule.get(op2).priority(); } throw new InvalidParameterException(String.format("Invalid parameter %s %s", op1, op2)); } /** * 計算逆波蘭表示式 * @param expression 基礎表示式 * @return 逆波蘭表示式元素集合 * @throws InvalidParameterException 在無法解析表示式時丟擲無效引數異常 */ public List<String> reversePolishNotation(String expression) throws InvalidParameterException { List<String> initial = decompositionFormula(expression); return reversePolishNotation(initial); } /** * 獲取表示式計算結果 * @param expression 表示式 * @return 表示式計算結果 * @throws InvalidParameterException 若表示式是無法解析的將會丟擲該異常 */ public BigDecimal getResult(String expression) throws InvalidParameterException { return getResult(reversePolishNotation(expression)); } /** * 獲取逆波蘭表示式的計算結果 * @param reversePolishExpression 逆波蘭表示式 * @return 逆波蘭表示式計算結果 * @throws InvalidParameterException 若逆波蘭表示式是無法解析的將會丟擲該異常 */ public BigDecimal getResult(List<String> reversePolishExpression) throws InvalidParameterException{ Stack<String> opera = new Stack<>(); for (String item : reversePolishExpression) { if (isSupportNumber(item)){ opera.push(item); }else if (isSupportOperator(item)){ opera.push(String.valueOf(calculation(opera.pop(), opera.pop(), item))); }else { throw new InvalidParameterException(reversePolishExpression.toString()); } } return new BigDecimal(opera.pop()).stripTrailingZeros(); } /** * 計算規則 * @param s1 雙目右值 * @param s2 雙目左值 * @param s3 運算子 * @return 結果 */ public BigDecimal calculation(String s1, String s2, String s3) { return rule.get(s3).method(s2, s1); } /** * 通過已分解的表示式求逆波蘭表示式 * @param expression 初始表示式 * @return 逆波蘭式 * @throws InvalidParameterException 若表示式元素集合無法被解析,這將會觸發無效引數異常 */ public List<String> reversePolishNotation(List<String> expression) throws InvalidParameterException { List<String> reversePolishExpression = new ArrayList<>(); Stack<String> operatorsStack = new Stack<>(); for (String item : expression) { if (isSupportNumber(item)){ reversePolishExpression.add(item); }else if (isSupportOperator(item)){ manageOperator(operatorsStack, reversePolishExpression, item); }else { throw new InvalidParameterException(expression.toString()); } } while (!operatorsStack.isEmpty()) { reversePolishExpression.add(operatorsStack.pop()); } return reversePolishExpression; } /** * 計算逆波蘭表示式核心 * @param operatorsStack 操作符棧 * @param reversePolishExpression 逆波蘭式結果集合 * @param item 當前要處理的元素項 * @throws InvalidParameterException 若元素無法被處理則將丟擲該異常 */ private void manageOperator(Stack<String> operatorsStack, List<String> reversePolishExpression, String item) throws InvalidParameterException { if (operatorsStack.isEmpty()){ operatorsStack.push(item); }else if ("(".equals(item)){ operatorsStack.push(item); }else if (")".equals(item)){ String string = ""; while (!operatorsStack.isEmpty() && !"(".equals(string = operatorsStack.pop())) { reversePolishExpression.add(string); } }else if ("(".equals(operatorsStack.peek())) { operatorsStack.push(item); } else if (getPriority(item, operatorsStack.peek())) { operatorsStack.push(item); }else { reversePolishExpression.add(operatorsStack.pop()); manageOperator(operatorsStack, reversePolishExpression, item); } } /** * 分解表示式 * @param expression 初始表示式 * @return 分解後的表示式 */ public List<String> decompositionFormula(String expression){ List<String> res = new ArrayList<>(); StringBuilder delim = new StringBuilder(); for (String s : rule.keySet()) { delim.append(s); } StringTokenizer decomposition = new StringTokenizer(expression, delim.toString(), true); while (decomposition.hasMoreTokens()) { res.add(decomposition.nextToken()); } return res; } /** * 支援的操作運算子 * @param operator 運算子 * @return 是否支援 */ public boolean isSupportOperator(String operator){ return rule.containsKey(operator); } /** * 是不是支援運算的數字或小數 * @param number 數字/小數 * @return 是否支援 */ public boolean isSupportNumber(String number){ return number.matches("([1-9]\\d*\\.?\\d*)|(0\\.\\d*[1-9])"); } }
但是此時我們還不能用它來計算,因為該計算器中還沒有運算子
構建基本四則運算
為了讓我進行演示,所以我這裡嘗試構建四則運算,以此作為示例
將計算器的無參預設建構函式替換為如下程式碼即可
public Calculators(){
// 特殊字元右括號為內建
rule.put(")", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return null;
}
@Override
public int priority() {
return Integer.MIN_VALUE;
}
});
// 定義加法規則
rule.put("+", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).add(new BigDecimal(val2));
}
@Override
public int priority() {
return 1;
}
});
// 定義減法規則
rule.put("-", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).subtract(new BigDecimal(val2));
}
@Override
public int priority() {
return 1;
}
});
// 定義乘法規則
rule.put("*", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).multiply(new BigDecimal(val2));
}
@Override
public int priority() {
return 2;
}
});
// 定義除法規則
rule.put("/", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).divide(new BigDecimal(val2), 32, RoundingMode.DOWN);
}
@Override
public int priority() {
return 2;
}
});
// 特殊字元左括號為內建
rule.put("(", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return null;
}
@Override
public int priority() {
return Integer.MAX_VALUE;
}
});
}
嘗試執行
嘗試在main方法中執行它
public static void main(String[] args) throws InvalidParameterException {
Calculators calculators = new Calculators();
String expression = "34*(23-(1+23)*4-3.24)*(23-32*(23-324)/(34+34))/(43-45*343)";
List<String> strings = calculators.reversePolishNotation(expression);
StringBuilder stringBuilder = new StringBuilder();
for (String string : strings) {
stringBuilder.append(string);
}
System.out.println("初始化表示式: " + expression);
System.out.println("逆波蘭表示式: " + stringBuilder.toString());
System.out.println("計算逆波蘭式: " + calculators.getResult(strings));
// 這裡只是為了方便看到轉換的全過程,所以列印了以下輸出等語句,實際使用過程中使用如下方式即可:
// Calculators calculators = new Calculators();
// String expression = "34*(23-(1+23)*4-3.24)*(23-32*(23-324)/(34+34))/(43-45*343)";
// System.out.println("計算結果: " + calculators.getResult(expression));
}
嘗試執行它,你會得到以下結果:
讓我們來使用windows自帶的計算器來驗證我們是否是正確的
結果是完全正確的,只是windows自帶的計算器保留了30位小數,而我的程式碼中保留了32位小數,這是在除法規則中規定的。
如果您不喜歡這麼長的小數位,可以去修改它
嘗試擴充套件簡單運算子
現在我們的程式碼已經可以處理基本的四則運算了,但是還有一個求餘運算子也是我們經常使用的,但是現在這個計算器還不具備這個功能,所以我們在外部來擴充套件它。
因為求餘操作非常簡單,所以直接實現該介面是一個非常簡單的方式
public static void main(String[] args) throws InvalidParameterException {
Calculators calculators = new Calculators();
// ----------------------簡單的擴充套件運算子例子-----------------------------
// 擴充套件後使得計算器支援求餘操作
calculators.addOrReplaceCalculatorRule("%", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).divideAndRemainder(new BigDecimal(val2))[1];
}
@Override
public int priority() {
return 2;
}
});
}
嘗試執行
public static void main(String[] args) throws InvalidParameterException {
Calculators calculators = new Calculators();
// ----------------------簡單的擴充套件運算子例子-----------------------------
// 擴充套件後使得計算器支援求餘操作
calculators.addOrReplaceCalculatorRule("%", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).divideAndRemainder(new BigDecimal(val2))[1];
}
@Override
public int priority() {
return 2;
}
});
// -------------------------嘗試計算包含求餘的表示式------------------------
String expression = "((34*(12%5)*3)*99)%21*3.1";
List<String> strings = calculators.reversePolishNotation(expression);
StringBuilder stringBuilder = new StringBuilder();
for (String string : strings) {
stringBuilder.append(string);
}
System.out.println("初始化表示式: " + expression);
System.out.println("逆波蘭表示式: " + stringBuilder.toString());
System.out.println("計算逆波蘭式: " + calculators.getResult(strings));
}
執行該例子,得到如下結果
還是用windows計算器驗證一下
這就是簡單的擴充套件操作符的簡單例子
定義更加複雜的運算子
這裡準備新增以下運算子,計算斐波那契數列的第N項,涉及到矩陣快速冪演算法,不瞭解的同學可以去學習一下。
因為當前我們寫的規則只適合雙目運算,所以我這個規定了該運算子的使用方式是以F作為運算子,然後表示式左值為斐波那契數列的開始項,表示式右值為從開始項開始數的第N項數值,如:
3F5
// 該表示式的意思就是從斐波那契數列第三項開始數的第五項
// 總體來說就是斐波那契數列的第7項
0 1 1 2 3 5 [8] 13 21
// 結果就是方括號中的值
對於一些更加複雜的操作,我們直接去new一個介面就不合適了,因為這樣會讓我們的程式碼變的非常複雜
這裡我們新建一個類,比如它叫FibonacciN 然後實現CalculatorRule介面並重寫兩個方法即可
package core;
import java.math.BigDecimal;
import java.security.InvalidParameterException;
public class FibonacciN implements CalculatorRule {
/**
* 定義2*2矩陣模型
*/
static class Matrix {
BigDecimal[][] source = new BigDecimal[2][2];
public Matrix(BigDecimal[][] other) {
if (other.length != this.source.length) {
throw new InvalidParameterException();
}
for (int x = 0; x < this.source.length; x++) {
System.arraycopy(other[x], 0, this.source[x], 0, this.source[x].length);
}
}
/**
* 矩陣相乘規則
*/
public Matrix multiply(Matrix other) {
if (other.source.length <= 0 || this.source.length <= 0 || other.source[0].length != this.source.length) {
throw new InvalidParameterException();
}
// 這裡提前就知道這肯定是2*2矩陣相乘程式碼,但是最好還是不要去硬寫,所以我這裡寫了通用的雙矩陣相乘
BigDecimal[][] A = this.source;
BigDecimal[][] B = other.source;
BigDecimal[][] res = new BigDecimal[A.length][B[0].length];
for (int x = 0; x < A.length; x++) {
for (int y = 0; y < A[x].length; y++) {
res[x][y] = new BigDecimal(0);
for (int k = 0; k < B.length; k++) {
res[x][y] = res[x][y].add(A[x][k].multiply(B[k][y]));
}
}
}
return new Matrix(res);
}
/**
* 矩陣快速冪
* @param matrix 矩陣
* @param N 第N項
* @return 矩陣冪結果
*/
private Matrix MatrixQuickPow(Matrix matrix, int N) {
Matrix res = new Matrix(new BigDecimal[][]{
{new BigDecimal(1), new BigDecimal(0)},
{new BigDecimal(0), new BigDecimal(1)}
});
while (N > 0) {
if ((N & 1) == 1) {
res = res.multiply(matrix);
}
matrix = matrix.multiply(matrix);
N >>= 1;
}
return res;
}
}
/**
* 雙目計算規則
* @param val1 雙目左值
* @param val2 雙目右值
* @return 表示式的值
* @throws InvalidParameterException 丟擲可能的無效引數異常
*/
@Override
public BigDecimal method(String val1, String val2) throws InvalidParameterException {
int start = 0;
int N = 0;
try {
start = Integer.parseInt(val1);
N = start + Integer.parseInt(val2);
if (start <= 0 || N <= 0){
throw new InvalidParameterException();
}
} catch (NumberFormatException exception) {
throw new InvalidParameterException();
}
Matrix res = new Matrix(new BigDecimal[][]{
{new BigDecimal(0), new BigDecimal(1)},
{new BigDecimal(1), new BigDecimal(1)}
});
res = res.MatrixQuickPow(res, N);
return res.source[0][0];
}
/**
* 這裡定義這個符號比乘除優先順序更高,這樣就能先計算該算式的結果了
* @return 優先順序
*/
@Override
public int priority() {
return 3;
}
}
嘗試執行
public static void main(String[] args) throws InvalidParameterException {
Calculators calculators = new Calculators();
// 擴充套件後使得計算器支援求餘操作
calculators.addOrReplaceCalculatorRule("%", new CalculatorRule() {
@Override
public BigDecimal method(String val1, String val2) {
return new BigDecimal(val1).divideAndRemainder(new BigDecimal(val2))[1];
}
@Override
public int priority() {
return 2;
}
});
// ----------------------複雜的擴充套件運算子例子-----------------------------
// 使用矩陣快速冪擴充套件斐波那契數列求第i項開始的第N項數值
calculators.addOrReplaceCalculatorRule("F", new FibonacciN());
// 表示式含義34 * (從第二項開始的第3項斐波那契數列的值也就是第4項 求餘 4)
String expression = "34*(3F5%5)*1.9";
List<String> strings = calculators.reversePolishNotation(expression);
StringBuilder stringBuilder = new StringBuilder();
for (String string : strings) {
stringBuilder.append(string);
}
System.out.println("初始化表示式: " + expression);
System.out.println("逆波蘭表示式: " + stringBuilder.toString());
System.out.println("計算逆波蘭式: " + calculators.getResult(strings));
}
嘗試執行定義了斐波那契數列運算子的例子
因為我們知道斐波那契數列第7項,所以這裡驗證的時候先把值寫死就好了!
如果您對這份程式碼有什麼問題或疑問,記得給我反饋哦,感謝~