四則運算 Java 實現 劉豐璨,王翠鸞
四則運算
GitHub倉庫
功能實現
- [x] 使用 -n 參數控制生成題目的個數,並且根據解空間限制用戶設定的範圍(如 range == 2 時,用戶卻要求生成 10000 道題目,這明顯不合理)
- [x] 使用 -r 參數控制題目中自然數、真分數、真分數分母的範圍 該參數可以設置為大於 2 的自然數
- [x] 生成的題目在計算過程不能產生負數
- [x] 生成的題目中如果存在形如e1 ÷ e2的子表達式,那麽其結果應是真分數
- [x] 程序一次運行生成的題目不能重復,生成的題目存入執行程序目錄下的Exercises.txt文件中
- [x] 每道題目中出現的運算符個數不超過3個(不包括括號)
- [x] 在生成題目的同時,計算出所有題目的答案,並存入執行程序的當前目錄下的Answers.txt
- [x] 程序應能支持一萬道題目的生成。
- [x] 程序支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計。
設計過程
表達式的生成
根據題目的要求,表達式中運算符不能超過 3 個,因此所生成的表達式種類可以歸納為以下四類:
- 單算子表達式
- 型如 A ? B 的表達式
- 在生成表達式時在** + × **這種擁有交換律的符號時,默認按照查重式 A < B 的順序排序查重即可
- 雙算子表達式
- 型如 (A ? B) ? C 的表達式
- 在生成表達式時只要保證括號內的符號滿足交換律時,默認按照查重式 A < B 的順序排序查重即可
- 三算子 A 型表達式
- 型如 ((A ? B) ? C) ? D) 的表達式
- 在生成表達式時,查重規則和雙算子表達式一樣,只需要保持查重式中 A < B 而無需顧慮 C 和 D,因為根據題目定義 ((A ? B) ? C) ? D) 和 ((A ? B) ? D) ? C) 是兩道不同的題目
- 三算子 B 型表達式
- 型如 (A ? B) ? (C ? D) 的表達式
- 在生成表達式時,保證查重式 A < B 且 C < D 且 A < C 即可。
上述表達中有幾點需要解釋:
查重式:在生成四則運算題目時,為了檢查題目重復而對題目書寫進行了特定排序的表達式。該表達式語義和實際題目是一樣的,而只為查重服務,可能不會出現在最終的題目裏。例如 1 + 2 就是 2 + 1 的查重式,(1 + 3) + (2 + 4) 是 (4 + 2) + (3 + 1) 的查重式
偷懶的寫法:因為實際上,長的表達式中包含了短的表達式,我們只需要寫好單算子表達式和雙算子表達式,後面的表達式生成函數就可以直接套用前面兩個生成器的模式,而減少了很多重復勞動。即如雙算子表達式裏的(A ? B)部分可以依靠單算子表達式生成器進行生成。
負數的避免
負數只會在減法運算時產生,每當遇到減法運算時,只需要檢查位於左側的表達式或分數大於等於位於右側的即可。否則,交換減法運算的左右操作數。
除法假分數的避免
除法假分數只會在位於右側的操作數大於位於左側的操作數時產生,只需要檢查位於左側的表達式小於或等於右側的即可。否則,交換除法運算的左右操作數。
操作數的表示
無論是真假分數還是整數,都可以當成分數來進行計算。在生成操作數的時候,我們就會使用一個分數生成器,按照特定規則,生成分數。而表達式的操作也是分數和分數之間的操作。直到最後生成表達式時,才會將分數進行轉化輸出。
判斷是否重復
因為查重式子能確保所有同義的運算式都能輸出為同一個算式,所以可以直接將查重式放進 HashSet 中,一旦查詢到一個查重式已經存在於 HashSet 中,則證明這個式子已經重復。否則則沒有重復。
代碼
分數類 Fraction
實現了分數的存儲、加減乘除基本運算、化簡、和字符串的相互轉化以及字符串算術式的計算等功能
import java.util.Stack;
/**
* 分數類
*/
public class Fraction {
/**
* 帶分數的分子
*/
private int numerator = 0;
/**
* 帶分數的分母
*/
private int denominator = 1;
/**
* 符號位(1表示整數,-1表示負數)
*/
private int symbol = 1;
public Fraction() {
}
public Fraction(int numerator, int denominator) {
if (denominator == 0) throw new ArithmeticException("分母不能為 0 ");
this.symbol = numerator * denominator < 0 ? -1 : 1;
this.numerator = Math.abs(numerator);
this.denominator = Math.abs(denominator);
this.simplify();
}
/**
* 求兩個數的最大公因數
*
* @param numberA 數A
* @param numberB 數B
* @return 返回最大公因數
*/
private static int gcd(int numberA, int numberB) {
if (numberA % numberB == 0) {
return numberB;
} else {
return gcd(numberB, numberA % numberB);
}
}
/**
* 求兩個數的最小公倍數
*
* @param numberA 數A
* @param numberB 數B
* @return 返回最小公倍數
*/
private static int lcm(int numberA, int numberB) {
return numberA * numberB / gcd(numberA, numberB);
}
/**
* 將分數進行化簡
*/
private void simplify() {
int div = gcd(numerator, denominator);
if(div != 1) {
numerator /= div;
denominator /= div;
}
}
/**
* 分數加法
*
* @param fraction 加數
* @return 返回分數類型結果
* 結果為真分數
*/
public Fraction add(Fraction fraction) {
// 先獲取兩個分數分母的最小公倍數
int mul = lcm(this.denominator, fraction.denominator);
int numerator = this.symbol * this.numerator * mul / this.denominator
+ fraction.symbol * fraction.numerator * mul / fraction.denominator;
return new Fraction(numerator, mul);
}
/**
* 分數減法
*
* @param fraction 減數
* @return 返回分數類型結果
* 結果為真分數
*/
public Fraction subtract(Fraction fraction) {
//先獲取兩個分數分母的最小公倍數
int mul = lcm(this.denominator, fraction.denominator);
int numerator = this.symbol * this.numerator * mul / this.denominator
- fraction.symbol * fraction.numerator * mul / fraction.denominator;
return new Fraction(numerator, mul);
}
/**
* 分數乘法
*
* @param fraction 乘數
* @return 返回分數類型結果
* 這裏的結果可能不為真分數
*/
public Fraction multiply(Fraction fraction) {
//這裏只需要隨便一個數表示符號
return new Fraction(this.numerator * fraction.numerator,
this.symbol * fraction.symbol * this.denominator * fraction.denominator);
}
/**
* 分數除法
*
* @param fraction 除數
* @return 返回分數類型結果
* 這裏的結果可能不為真分數
*/
public Fraction divide(Fraction fraction) {
if (fraction.numerator == 0)
throw new ArithmeticException("除數不能為 0 ");
Fraction f = new Fraction(fraction.denominator, fraction.numerator);
return this.multiply(f);
}
/**
* 判斷該分數是否比輸入分數大
* @param fraction 輸入分數
* @return true 為大, false 為小於等於
*/
public boolean isGreaterThan(Fraction fraction) {
Fraction result = subtract(fraction);
if(result.symbol == -1) return false;
else if(result.numerator == 0) return false;
return true;
}
@Override
public String toString() {
StringBuilder fraction = new StringBuilder();
//先處理符號問題
if (symbol == -1)
fraction.append("-");
if (numerator == 0) {
fraction.append("0");
} else if (numerator > denominator) {
//如果分子比分母大,化為真分數
fraction.append(numerator / denominator);
if (numerator % denominator != 0) {
fraction.append("‘");
fraction.append(numerator % denominator);
fraction.append("/");
fraction.append(denominator);
}
} else if (numerator < denominator) {
fraction.append(numerator);
fraction.append("/");
fraction.append(denominator);
} else {
fraction.append(1);
}
return fraction.toString();
}
/**
* 將String轉化為Fraction
*
* @param s String
* @return Fraction分數
*/
public static Fraction string2Fraction(String s) {
int numerator = 0;
int denominator = 1;
//這裏取‘之後的字符串進行操作
String[] strings = s.substring(s.indexOf("‘") + 1).split("/");
if (strings.length <= 0) throw new NumberFormatException();
numerator = Integer.parseInt(strings[0]);
if (strings.length >= 2) denominator = Integer.parseInt(strings[1]);
//操作‘之前的
if (s.contains("‘")) numerator = Integer.parseInt(s.substring(0, s.indexOf("‘"))) * denominator + numerator;
return new Fraction(numerator, denominator);
}
/**
* 計算字符串四則運算式的函數
* @param sIn String 表示的四則運算式
* @return 該四則運算式的答案
*/
public static Fraction calculateStringExp(String sIn) {
Stack<Fraction> fractionStack = new Stack<>();
Stack<Character> symbolStack = new Stack<>();
String s = sIn.replaceAll(" ","");
s += "=";
StringBuffer temp = new StringBuffer();
for(int i = 0; i < s.length();i++) {
char ch = s.charAt(i);
if((ch >= ‘0‘ && ch <= ‘9‘) || ch == ‘\‘‘ || ch == ‘/‘)
temp.append(ch);
else {
String tempStr = temp.toString();
if(!tempStr.isEmpty()) {
Fraction f = Fraction.string2Fraction(tempStr);
fractionStack.push(f);
temp = new StringBuffer();
}
while(!comparePriority(ch, symbolStack) && !symbolStack.empty()) {
Fraction numberB = fractionStack.pop();
Fraction numberA = fractionStack.pop();
switch(symbolStack.pop()) {
case ‘+‘:
fractionStack.push(numberA.add(numberB));
break;
case ‘-‘:
fractionStack.push(numberA.subtract(numberB));
break;
case ‘ב:
fractionStack.push(numberA.multiply(numberB));
break;
case ‘÷‘:
fractionStack.push(numberA.divide(numberB));
break;
default:
break;
}
}
if(ch != ‘=‘) {
symbolStack.push(ch);
if (ch == ‘)‘) {
symbolStack.pop();
symbolStack.pop();
}
}
}
}
return fractionStack.pop();
}
/**
* 判定符號優先級的類
* @param symbol 符號
* @param symbolStack 符號棧
* @return
*/
private static boolean comparePriority(char symbol, Stack<Character> symbolStack) {
if (symbolStack.empty()) return true;
// 符號優先級說明(從高到低): ( 大於 × ÷ 大於 + - 大於 )
char top = symbolStack.peek();
if (top == ‘(‘) return true;
switch (symbol) {
case ‘(‘: return true;
case ‘ב:
if (top == ‘+‘ || top == ‘-‘) return true;
else return false;
case ‘÷‘:
if (top == ‘+‘ || top == ‘-‘) return true;
else return false;
case ‘+‘: return false;
case ‘-‘: return false;
case ‘)‘: return false;
case ‘=‘: return false;
default: break;
}
return true;
}
/**
* 判斷這個分數值是否為 0
* @return
*/
public boolean isZero() {
return numerator == 0;
}
}
ExerciseGenerator 算術式生成器
定義了各類負責隨機生成的函數
- 隨機生成分數
- 隨機生成符號
- 隨機生成單算子表達式
- 隨機生成二算子表達式
- 隨機生成三算子 A 型表達式
- 隨機生成三算子 B 型表達式
- 查重式的查重
- 最終表達式和答案的輸出
import java.util.*;
// 算數題目生成
public class ExerciseGenerator {
/**
* 數值範圍
*/
private int range;
/**
* 題目數量
*/
private int number;
/**
* 隨機數種子,用於生成各種隨機選項
*/
private Random random = new Random(System.currentTimeMillis());
/**
* 驗證重復使用的哈希集合
*/
private Set<String> duplicateCheckSet = new HashSet<>();
/**
* 存儲答案使用的哈希映射表 <算式, 答案>
*/
private HashMap<String, String> resultMap = new HashMap<>();
public ExerciseGenerator(){}
public ExerciseGenerator(int range, int number){
if(range < 2) throw new IllegalArgumentException("數值的範圍不能小於 2 ");
if(number < 1) throw new IllegalArgumentException("題目的數量不能小於 1 ");
this.range = range;
this.number = number;
}
/**
* 生成算術式和答案的類
* @return 當對應 range 可以生成 number 個算術式時,返回表達式和答案;否則返回 null
*/
public HashMap<String,String> generateExp() {
if(number > 1000*(range-1)*(range-1)) return null;
duplicateCheckSet = new HashSet<>();
resultMap = new HashMap<>();
int allo = 0;
String[] answer;
while(duplicateCheckSet.size() < number) {
allo = random.nextInt(101);
if(allo <= 1) answer = oneOpGenerator();
else if(allo <= 10) answer = twoOpGenerator();
else if(allo <= 55) answer = threeOpGeneratorA();
else answer = threeOpGeneratorB();
if(duplicateCheck(answer[0])) resultMap.put(answer[1], answer[2]);
}
return resultMap;
}
public boolean duplicateCheck(String expression) {
if(duplicateCheckSet.contains(expression)) return false;
duplicateCheckSet.add(expression);
return true;
}
/**
* 數值生成器
*
* @return 分數對象
*/
public Fraction fractionGenerator() {
if(range < 1) throw new IllegalArgumentException("數值的範圍不能小於 1 ");
// 分母範圍為 1 - range, 不包含 range
int denominator = random.nextInt(range - 1) + 1;
int numerator = 0;
// 分子範圍為 0 - range ^ 2, 不包含 range ^ 2
numerator = random.nextInt(denominator * denominator + 1);
return new Fraction(numerator, denominator);
}
/**
* 算子生成器
* @return + - × ÷ 中的任一一個運算符
*/
public char opGenerator() {
char result;
switch(random.nextInt(4)) {
case 0:
result = ‘+‘;
break;
case 1:
result = ‘-‘;
break;
case 2:
result = ‘ב;
break;
case 3:
result = ‘÷‘;
break;
default:
result = ‘+‘;
}
return result;
}
/**
* 單算子算術式生成器
* @return 字符串數組,[0]放置化簡的查重字符串,[1]放置混淆後的實際字符串,[2]放置算術式答案,[3]放置本次算術式生成的運算符
*/
public String[] oneOpGenerator() {
String[] result = new String[4];
Fraction numberA = fractionGenerator();
Fraction numberB = fractionGenerator();
switch(opGenerator()) {
case ‘-‘:
result[3] = "-";
// 將大的數放在前面
if(!numberA.isGreaterThan(numberB)) {
Fraction temp = numberA;
numberA = numberB;
numberB = temp;
}
// 對於減法的情況,化簡的查重字符串和混淆字符串是一樣的
result[0] = numberA.toString() + " - " + numberB.toString();
result[1] = numberA.toString() + " - " + numberB.toString();
result[2] = numberA.subtract(numberB).toString();
break;
case ‘÷‘:
result[3] = "÷";
// 保證除法分母不為 0
while(numberB.isZero() || numberA.isGreaterThan(numberB))
numberB = fractionGenerator();
// 對於減法的情況,化簡的查重字符串和混淆字符串是一樣的
result[0] = numberA.toString() + " ÷ " + numberB.toString();
result[1] = numberA.toString() + " ÷ " + numberB.toString();
result[2] = numberA.divide(numberB).toString();
break;
case ‘+‘:
result[3] = "+";
// 對於加法的情況,查重字符串要把數從小到大排,而混淆字符串則不需要
if(numberA.isGreaterThan(numberB)) result[0] = numberB.toString() + " + " + numberA.toString();
else result[0] = numberA.toString() + " + " + numberB.toString();
result[1] = numberA.toString() + " + " + numberB.toString();
result[2] = numberA.add(numberB).toString();
break;
case ‘ב:
result[3] = "×";
// 對於乘法的情況,查重字符串要把數從小到大排,而混淆字符串則不需要
if(numberA.isGreaterThan(numberB)) result[0] = numberB.toString() + " × " + numberA.toString();
else result[0] = numberA.toString() + " × " + numberB.toString();
result[1] = numberA.toString() + " × " + numberB.toString();
result[2] = numberA.multiply(numberB).toString();
break;
}
return result;
}
/**
* 二算子算術式生成器
* @return 字符串數組,[0]放置化簡的查重字符串,[1]放置混淆後的實際字符串,[2]放置算術式答案,[3]放置本次算術式生成的運算符
*/
public String[] twoOpGenerator() {
// 生成一個單算子算術式
String[] result = oneOpGenerator();
Fraction numberA = Fraction.string2Fraction(result[2]);
Fraction numberB = fractionGenerator();
switch(opGenerator()){
case ‘+‘:
result[0] = result[0] + " + " + numberB.toString();
result[2] = numberA.add(numberB).toString();
// 隨機決定是否擾亂次序
if(random.nextInt(2) == 0) {
// 不擾亂次序
result[1] = result[1] + " + " + numberB.toString();
}
else {
// 擾亂次序要檢查符號優先級,如果優先級大,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[1] = numberB.toString() + " + " + result[1];
else result[1] = numberB.toString() + " + (" + result[1] + ")";
}
result[3] = "+";
break;
case ‘-‘:
// 如果前面的數比後面的數小,則需要調換順序
if(!numberA.isGreaterThan(numberB)) {
// 如果原本表達式符號優先級高,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")){
result[0] = numberB.toString() + " - " + result[0];
result[1] = numberB.toString() + " - " + result[1];
result[2] = numberB.subtract(numberA).toString();
}
// 如果原本表達式的優先級和當前的相同,則需要加括號
else {
result[0] = numberB.toString() + " - (" + result[0] + ")";
result[1] = numberB.toString() + " - (" + result[1] + ")";
result[2] = numberB.subtract(numberA).toString();
}
} else {
result[0] = result[0] + " - " + numberB.toString();
result[1] = result[1] + " - " + numberB.toString();
result[2] = numberA.subtract(numberB).toString();
}
result[3] = "-";
break;
case ‘ב:
// 如果原算術式優先級和 × 相等,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[0] = result[0] + " × " + numberB.toString();
else result[0] = "(" + result[0] + ") × " + numberB.toString();
result[2] = numberA.multiply(numberB).toString();
// 隨機決定是否擾亂次序
if(random.nextInt(2) == 0) {
// 如果遇到加減要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[1] = result[1] + " × " + numberB.toString();
else result[1] = "(" + result[1] + ") × " + numberB.toString();
}
else {
// 因為 × 的優先級大,所以擾亂次序在任何情況下都要加括號
result[1] = numberB.toString() + " × (" + result[1] + ")";
}
result[3] = "×";
break;
case ‘÷‘:
while(numberB.isZero())
numberB = fractionGenerator();
// 如果之前的算術式結果為 0,則必須調換位置;後面的隨機數決定在符合條件的時候是否更換次序以加大題目隨機性
if(numberA.isZero() || numberB.isGreaterThan(numberA) || random.nextInt(2) == 0) {
// 如果原來的式子優先級低
if(result[3].equals("+") || result[3].equals("-")) {
result[0] = "(" + result[0] + ") ÷ " + numberB.toString();
result[1] = "(" + result[1] + ") ÷ " + numberB.toString();
result[2] = numberA.divide(numberB).toString();
}
else {
result[0] = result[0] + " ÷ " + numberB.toString();
result[1] = result[1] + " ÷ " + numberB.toString();
result[2] = numberA.divide(numberB).toString();
}
} else {
// 如果更換次序,則無論如何都必須加括號
result[0] = numberB.toString() + " ÷ (" + result[0] + ")";
result[1] = numberB.toString() + " ÷ (" + result[1] + ")";
result[2] = numberB.divide(numberA).toString();
}
result[3] = "÷";
break;
}
return result;
}
/**
* 三算子算術式生成器 - A型(3+1)
* @return 字符串數組,[0]放置化簡的查重字符串,[1]放置混淆後的實際字符串,[2]放置算術式答案,[3]放置本次算術式生成的運算符
*/
public String[] threeOpGeneratorA() {
// 生成一個雙算子算術式
String[] result = twoOpGenerator();
Fraction numberA = Fraction.string2Fraction(result[2]);
Fraction numberB = fractionGenerator();
switch(opGenerator()){
case ‘+‘:
result[0] = result[0] + " + " + numberB.toString();
result[2] = numberA.add(numberB).toString();
// 隨機決定是否擾亂次序
if(random.nextInt(2) == 0) {
// 不擾亂次序
result[1] = result[1] + " + " + numberB.toString();
}
else {
// 擾亂次序要檢查符號優先級,如果優先級大,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[1] = numberB.toString() + " + " + result[1];
else result[1] = numberB.toString() + " + (" + result[1] + ")";
}
result[3] = "+";
break;
case ‘-‘:
// 如果前面的數比後面的數小,則需要調換順序
if(!numberA.isGreaterThan(numberB)) {
// 如果原本表達式符號優先級高,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")){
result[0] = numberB.toString() + " - " + result[0];
result[1] = numberB.toString() + " - " + result[1];
result[2] = numberB.subtract(numberA).toString();
}
// 如果原本表達式的優先級和當前的相同,則需要加括號
else {
result[0] = numberB.toString() + " - (" + result[0] + ")";
result[1] = numberB.toString() + " - (" + result[1] + ")";
result[2] = numberB.subtract(numberA).toString();
}
} else {
result[0] = result[0] + " - " + numberB.toString();
result[1] = result[1] + " - " + numberB.toString();
result[2] = numberA.subtract(numberB).toString();
}
result[3] = "-";
break;
case ‘ב:
// 如果原算術式優先級和 × 相等,則不需要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[0] = result[0] + " × " + numberB.toString();
else result[0] = "(" + result[0] + ") × " + numberB.toString();
result[2] = numberA.multiply(numberB).toString();
// 隨機決定是否擾亂次序
if(random.nextInt(2) == 0) {
// 如果遇到加減要加括號
if(result[3].equals("×") || result[3].equals("÷")) result[1] = result[1] + " × " + numberB.toString();
else result[1] = "(" + result[1] + ") × " + numberB.toString();
}
else {
// 因為 × 的優先級大,所以擾亂次序在任何情況下都要加括號
result[1] = numberB.toString() + " × (" + result[1] + ")";
}
result[3] = "×";
break;
case ‘÷‘:
while(numberB.isZero())
numberB = fractionGenerator();
// 如果之前的算術式結果為 0,則必須調換位置;後面的隨機數決定在符合條件的時候是否更換次序以加大題目隨機性
if(numberA.isZero() || numberB.isGreaterThan(numberA) || random.nextInt(2) == 0) {
// 如果原來的式子優先級低
if(result[3].equals("+") || result[3].equals("-")) {
result[0] = "(" + result[0] + ") ÷ " + numberB.toString();
result[1] = "(" + result[1] + ") ÷ " + numberB.toString();
result[2] = numberA.divide(numberB).toString();
}
else {
result[0] = result[0] + " ÷ " + numberB.toString();
result[1] = result[1] + " ÷ " + numberB.toString();
result[2] = numberA.divide(numberB).toString();
}
} else {
// 如果更換次序,則無論如何都必須加括號
result[0] = numberB.toString() + " ÷ (" + result[0] + ")";
result[1] = numberB.toString() + " ÷ (" + result[1] + ")";
result[2] = numberB.divide(numberA).toString();
}
result[3] = "÷";
break;
}
return result;
}
/**
* 三算子算術式生成器 - B型(2+2)
* @return 字符串數組,[0]放置化簡後的字符串,[1]放置化簡前的字符串,[2]放置算術式答案
*/
public String[] threeOpGeneratorB() {
String[] result = new String[4];
String[] expA = oneOpGenerator();
String[] expB = oneOpGenerator();
Fraction aheadA = Fraction.string2Fraction(expA[0].substring(0, expA[0].indexOf(" ")));
Fraction aheadB = Fraction.string2Fraction(expB[0].substring(0, expB[0].indexOf(" ")));
if(aheadA.isGreaterThan(aheadB)) {
String[] temp = expA;
expA = expB;
expB = temp;
}
Fraction numberA = Fraction.string2Fraction(expA[2]);
Fraction numberB = Fraction.string2Fraction(expB[2]);
switch(opGenerator()) {
case ‘-‘:
// 將大的數放在前面
if(!numberA.isGreaterThan(numberB)) {
result[0] = "(" + expB[0] + ")" + " - " + "(" + expA[0] + ")";
result[1] = "(" + expB[1] + ")" + " - " + "(" + expA[1] + ")";
result[2] = numberB.subtract(numberA).toString();
}else {
// 對於減法的情況,化簡的查重字符串和混淆字符串是一樣的
result[0] = "(" + expA[0] + ")" + " - " + "(" + expB[0] + ")";
result[1] = "(" + expA[1] + ")" + " - " + "(" + expB[1] + ")";
result[2] = numberA.subtract(numberB).toString();
}
result[3] = "-";
break;
case ‘÷‘:
// 保證除法分母不為 0
while(numberB.isZero() || numberA.isGreaterThan(numberB)){
expB = oneOpGenerator();
numberB = Fraction.string2Fraction(expB[2]);
}
// 對於減法的情況,化簡的查重字符串和混淆字符串是一樣的
result[0] = "(" + expA[0] + ")" + " ÷ " + "(" + expB[0] + ")";
result[1] = "(" + expA[1] + ")" + " ÷ " + "(" + expB[1] + ")";
result[2] = numberA.divide(numberB).toString();
result[3] = "÷";
break;
case ‘+‘:
// 對於加法的情況,查重字符串要把數從小到大排,而混淆字符串則不需要
if(numberA.isGreaterThan(numberB)) result[0] = "(" + expB[0] + ")" + " + " + "(" + expA[0] + ")";
else result[0] = "(" + expA[0] + ")" + " + " + "(" + expB[0] + ")";
result[1] = "(" + expA[1] + ")" + " + " + "(" + expB[1] + ")";
result[2] = numberA.add(numberB).toString();
result[3] = "+";
break;
case ‘ב:
// 對於乘法的情況,查重字符串要把數從小到大排,而混淆字符串則不需要
if(numberA.isGreaterThan(numberB)) result[0] = "(" + expB[0] + ")" + " × " + "(" + expA[0] + ")";
else result[0] = "(" + expA[0] + ")" + " × " + "(" + expB[0] + ")";
result[1] = "(" + expA[1] + ")" + " × " + "(" + expB[1] + ")";
result[2] = numberA.multiply(numberB).toString();
result[3] = "×";
break;
}
return result;
}
}
UserInterface 用戶接口類
負責進行題目的生成和作業的批改
import java.beans.Transient;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class UserInterface {
public static void mainTest(String args[]) {
int number = 0;
int range = 0;
String exercise = null;
String answer = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-n")) number = Integer.valueOf(args[++i]);
if (args[i].equals("-r")) range = Integer.valueOf(args[++i]);
if (args[i].equals("-e")) exercise = args[++i];
if (args[i].equals("-a")) answer = args[++i];
}
if (number > 0 && range > 1) {
ExerciseGenerator exGen = new ExerciseGenerator(range, number);
HashMap<String, String> result = exGen.generateExp();
File exerciseFile = new File("Exercises.txt");
File answerFile = new File("Answers.txt");
FileWriter exerciseFileFW = null;
FileWriter answerFileFW = null;
try {
exerciseFileFW = new FileWriter(exerciseFile);
answerFileFW = new FileWriter(answerFile);
if (result != null) {
int i = 1;
for (Map.Entry<String, String> entry : result.entrySet()) {
exerciseFileFW.append(i + ". " + entry.getKey() + "\r\n");
answerFileFW.append(i + ". " + entry.getValue() + "\r\n");
i++;
exerciseFileFW.flush();
answerFileFW.flush();
}
System.out.println("完成出題。");
} else {
System.out.println("請減小出題數量或者增大數值範圍。");
}
} catch (IOException e) {
System.out.println("請檢查該目錄是否可寫。");
}
}
if (exercise != null && answer != null) {
StringBuilder correct = new StringBuilder("");
StringBuilder wrong = new StringBuilder("");
int correctInt = 0;
int wrongInt = 0;
File exercises = new File(exercise);
File answers = new File(answer);
File grades = new File("Grade.txt");
BufferedReader exercisesReader = null;
BufferedReader answersReader = null;
String e = null;
String a = null;
String t = null;
try {
exercisesReader = new BufferedReader(new FileReader(exercises));
answersReader = new BufferedReader(new FileReader(answers));
while((e=exercisesReader.readLine())!=null){
a = answersReader.readLine();
t = e.substring(0, e.indexOf("."));
if(Fraction.calculateStringExp(e.substring(e.indexOf(" ") + 1))
.toString()
.equals(a.substring(a.indexOf(" ") + 1))) {
correct.append(t).append(",");
correctInt++;
}
else {
wrong.append(t).append(",");
wrongInt++;
}
}
if(correct.length()>0) correct.deleteCharAt(correct.length()-1);
if(wrong.length()>0) wrong.deleteCharAt(wrong.length()-1);
FileWriter gradesFW = new FileWriter(grades);
gradesFW.append("Correct: ");
gradesFW.append(correctInt + "(");
gradesFW.append(correct.toString() + ")");
gradesFW.append("\r\n");
gradesFW.flush();
gradesFW.append("Wrong: ");
gradesFW.append(wrongInt + "(");
gradesFW.append(wrong.toString() + ")");
gradesFW.append("\r\n");
gradesFW.flush();
}
catch(Exception ex){
System.out.println("批改程序出錯,請檢查輸入的文件名是否正確。");
}
System.out.println("完成批改。");
}
}
}
測試
本項目所有測試都使用 Junit 進行,測試覆蓋了所有的方法。
FractionS2FTest 測試字符串<->分數互換功能
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(value=Parameterized.class)
public class FractionS2FTest {
private String input;
private String output;
@Parameterized.Parameters
public static Collection data() {
return Arrays.asList(new Object[][] {
{"0","0"},
{"1/4","1/4"},
{"2/4","1/2"},
{"3/4","3/4"},
{"4/4","1"},
{"5/4","1‘1/4"},
{"6/4","1‘1/2"},
{"7/4","1‘3/4"},
{"8/4","2"},
{"9/4","2‘1/4"},
{"10/4","2‘1/2"},
{"11/4","2‘3/4"},
{"12/4","3"},
{"13/4","3‘1/4"},
{"14/4","3‘1/2"},
{"15/4","3‘3/4"}
});
}
public FractionS2FTest(String input, String output) {
this.input = input;
this.output = output;
}
@Test
public void test() {
System.out.println("Input: " + input + " Output: " + output);
assertEquals(Fraction.string2Fraction(input).toString(), output);
assertEquals(Fraction.string2Fraction(output).toString(), output);
}
}
FractionConstructorTest 測試分數構造函數和化簡功能
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(value=Parameterized.class)
public class FractionConstructorTest {
private String expected;
private int numerator;
private int denominator;
@Parameterized.Parameters
public static Collection data() {
return Arrays.asList(new Object[][] {
{0,4,"0"},
{1,4,"1/4"},
{2,4,"1/2"},
{3,4,"3/4"},
{4,4,"1"},
{5,4,"1‘1/4"},
{6,4,"1‘1/2"},
{7,4,"1‘3/4"},
{8,4,"2"},
{9,4,"2‘1/4"},
{10,4,"2‘1/2"},
{11,4,"2‘3/4"},
{12,4,"3"},
{13,4,"3‘1/4"},
{14,4,"3‘1/2"},
{15,4,"3‘3/4"}
});
}
public FractionConstructorTest(int numerator, int denominator, String expected) {
this.expected = expected;
this.numerator = numerator;
this.denominator = denominator;
}
@Test
public void test() {
String result = new Fraction(numerator,denominator).toString();
System.out.println("The input fraction is: " + numerator + "/" + denominator + " and expression is: " + result);
assertEquals(expected, result);
}
}
FractionArithmeticTest 測試分數四則運算和算術式整體運算功能
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class FractionArithmeticTest {
@Test
public void addTest() {
assertEquals(Fraction.string2Fraction("44/5").toString(), Fraction.string2Fraction("8/1").add(Fraction.string2Fraction("4/5")).toString());
assertEquals(Fraction.string2Fraction("2/1").toString(), Fraction.string2Fraction("0/4").add(Fraction.string2Fraction("6/3")).toString());
assertEquals(Fraction.string2Fraction("79/24").toString(), Fraction.string2Fraction("8/3").add(Fraction.string2Fraction("5/8")).toString());
assertEquals(Fraction.string2Fraction("2/1").toString(), Fraction.string2Fraction("8/6").add(Fraction.string2Fraction("4/6")).toString());
assertEquals(Fraction.string2Fraction("3/7").toString(), Fraction.string2Fraction("3/7").add(Fraction.string2Fraction("0/5")).toString());
}
@Test
public void subtractTest() {
assertEquals(Fraction.string2Fraction("47/56").toString(), Fraction.string2Fraction("9/8").subtract(Fraction.string2Fraction("2/7")).toString());
assertEquals(Fraction.string2Fraction("7/6").toString(), Fraction.string2Fraction("5/3").subtract(Fraction.string2Fraction("1/2")).toString());
assertEquals(Fraction.string2Fraction("1/1").toString(), Fraction.string2Fraction("2/1").subtract(Fraction.string2Fraction("6/6")).toString());
assertEquals(Fraction.string2Fraction("1/2").toString(), Fraction.string2Fraction("2/3").subtract(Fraction.string2Fraction("1/6")).toString());
assertEquals(Fraction.string2Fraction("1/2").toString(), Fraction.string2Fraction("3/6").subtract(Fraction.string2Fraction("0/3")).toString());
}
@Test
public void multiplyTest() {
assertEquals(Fraction.string2Fraction("0/1").toString(), Fraction.string2Fraction("0/9").multiply(Fraction.string2Fraction("8/4")).toString());
assertEquals(Fraction.string2Fraction("0/1").toString(), Fraction.string2Fraction("6/5").multiply(Fraction.string2Fraction("0/4")).toString());
assertEquals(Fraction.string2Fraction("16/63").toString(), Fraction.string2Fraction("4/9").multiply(Fraction.string2Fraction("4/7")).toString());
assertEquals(Fraction.string2Fraction("7/8").toString(), Fraction.string2Fraction("6/8").multiply(Fraction.string2Fraction("7/6")).toString());
assertEquals(Fraction.string2Fraction("28/27").toString(), Fraction.string2Fraction("8/9").multiply(Fraction.string2Fraction("7/6")).toString());
}
@Test
public void divideTest() {
assertEquals(Fraction.string2Fraction("0/1").toString(), Fraction.string2Fraction("0/7").divide(Fraction.string2Fraction("13/4")).toString());
assertEquals(Fraction.string2Fraction("7/5").toString(), Fraction.string2Fraction("3/5").divide(Fraction.string2Fraction("3/7")).toString());
assertEquals(Fraction.string2Fraction("12/5").toString(), Fraction.string2Fraction("6/1").divide(Fraction.string2Fraction("5/2")).toString());
assertEquals(Fraction.string2Fraction("32/9").toString(), Fraction.string2Fraction("8/3").divide(Fraction.string2Fraction("3/4")).toString());
assertEquals(Fraction.string2Fraction("15/4").toString(), Fraction.string2Fraction("6/8").divide(Fraction.string2Fraction("1/5")).toString());
}
@Test
public void isZeroTest() {
assertEquals(true, Fraction.string2Fraction("0/4").isZero());
}
@Test
public void isGreaterThanTest() {
assertEquals(true, Fraction.string2Fraction("5/4").isGreaterThan(Fraction.string2Fraction("1/6")));
assertEquals(false, Fraction.string2Fraction("1/2").isGreaterThan(Fraction.string2Fraction("1/2")));
assertEquals(false, Fraction.string2Fraction("2/8").isGreaterThan(Fraction.string2Fraction("3/8")));
}
@Test
public void calculateStringExpTest() {
assertEquals("60‘43/60", Fraction.calculateStringExp("(4/5 + 3‘1/4) + (8‘8/9 × 6‘3/8)").toString());
assertEquals("0", Fraction.calculateStringExp("(1 × 0) ÷ (6 + 3/8)").toString());
assertEquals("49‘205/294", Fraction.calculateStringExp("2‘5/7 × (3‘1/2 + (6‘1/7 + 8‘2/3))").toString());
assertEquals("5‘1/10", Fraction.calculateStringExp("3‘2/3 × 3 - 1‘1/2 - 4‘2/5").toString());
assertEquals("0", Fraction.calculateStringExp("(4‘1/2 - (1/2 + 4)) × 0").toString());
assertEquals("1", Fraction.calculateStringExp("(5/7 + 1‘1/4) - (1‘5/7 - 3/4)").toString());
assertEquals("196/375", Fraction.calculateStringExp("4‘1/5 × ((4/5 - 1/3) ÷ 3‘3/4)").toString());
assertEquals("3/7", Fraction.calculateStringExp("(1‘3/4 × 1‘1/3) ÷ (3‘4/9 + 2)").toString());
assertEquals("15‘7/9", Fraction.calculateStringExp("2 × (4‘8/9 + 3) - 0").toString());
assertEquals("23/35", Fraction.calculateStringExp("(1 + 5‘4/7) ÷ (4 × 2‘1/2)").toString());
assertEquals("10/107", Fraction.calculateStringExp("1 ÷ (2‘3/4 + 2‘3/5) × 1/2").toString());
assertEquals("1‘1/42",Fraction.calculateStringExp("2‘6/7 - 2‘2/3 - 1/6 + 1").toString());
assertEquals("1‘26/35", Fraction.calculateStringExp("(3‘3/5 - 1‘6/7) - (0 × 1/2)").toString());
assertEquals("0", Fraction.calculateStringExp("1‘1/6 × 5‘1/7 × 0 + 0").toString());
assertEquals("1‘17/114", Fraction.calculateStringExp("1‘1/5 ÷ 3‘4/5 + 4 - 3‘1/6").toString());
assertEquals("44/45", Fraction.calculateStringExp("4‘8/9 ÷ 5 × 1 ÷ 1").toString());
assertEquals("65‘1/3", Fraction.calculateStringExp("(5‘1/3 + 0) × (4‘3/8 × 2‘4/5)").toString());
assertEquals("2‘4/7", Fraction.calculateStringExp("2 ÷ 3‘1/2 + 2").toString());
assertEquals("24‘17/60", Fraction.calculateStringExp("1‘1/5 + 5‘2/3 × 3‘1/2 + 3‘1/4").toString());
assertEquals("5‘7/12", Fraction.calculateStringExp("(4 - 2‘1/4) + (4‘5/6 - 1)").toString());
}
}
FractionMasterTest 分數測試匯總類,把上方三個測試歸一調用
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(value = Suite.class)
@Suite.SuiteClasses(value={
FractionArithmeticTest.class,
FractionConstructorTest.class,
FractionS2FTest.class})
public class FractionMasterTest {
}
ExerciseGeneratorTest 測試算術式生成類
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ExerciseGeneratorTest {
ExerciseGenerator exGen;
public ExerciseGeneratorTest() {
exGen = new ExerciseGenerator(10, 1000);
}
@Test
public void fractionGeneratorTest() {
Fraction f;
for(int i = 0;i < 100;i++) {
f = exGen.fractionGenerator();
assertTrue(Fraction.string2Fraction("10/1").isGreaterThan(f));
assertFalse(Fraction.string2Fraction("0/1").isGreaterThan(f));
}
}
@Test
public void opGeneratorTest() {
int add = 0;
int sub = 0;
int mul = 0;
int div = 0;
for(int i = 0;i < 100;i++) {
switch(exGen.opGenerator()){
case ‘+‘: add++; break;
case ‘-‘: sub++; break;
case ‘ב: mul++; break;
case ‘÷‘: div++; break;
}
}
assertTrue(sub > 0);
assertTrue(add > 0);
assertTrue(mul > 0);
assertTrue(div > 0);
}
@Test
public void oneOpGeneratorTest() {
String[] ss;
for(int i = 0;i < 100;i++) {
ss = exGen.oneOpGenerator();
for(String s:ss){
System.out.println(s);
}
System.out.println();
}
}
@Test
public void twoOpGeneratorTest() {
String[] ss;
for(int i = 0;i < 100;i++) {
ss = exGen.twoOpGenerator();
for(String s:ss){
System.out.println(s);
}
System.out.println();
}
}
@Test
public void threeOpGeneratorATest() {
String[] ss;
for(int i = 0;i < 100;i++) {
ss = exGen.threeOpGeneratorA();
for(String s:ss){
System.out.println(s);
}
System.out.println();
}
}
@Test
public void threeOpGeneratorBTest() {
String[] ss;
for(int i = 0;i < 100;i++) {
ss = exGen.threeOpGeneratorB();
for(String s:ss){
System.out.println(s);
}
System.out.println();
}
}
@Test
public void duplicateCheckTest() {
String[] ss;
int saveOne = 0;
int saveTwo = 0;
int saveThreeA = 0;
int saveThreeB = 0;
for(int i = 0;i < 100000;i++) {
if(i < 1000) {
ss = exGen.oneOpGenerator();
if (exGen.duplicateCheck(ss[0])) {
System.out.println("這是第 " + (i + 1) + " 個算術式");
for (String s : ss) {
System.out.println(s);
}
saveOne++;
}
}
else if(i < 10000){
ss = exGen.twoOpGenerator();
if (exGen.duplicateCheck(ss[0])) {
System.out.println("這是第 " + (i + 1) + " 個算術式");
for (String s : ss) {
System.out.println(s);
}
saveTwo++;
}
}
else if(i < 50000){
ss = exGen.threeOpGeneratorA();
if (exGen.duplicateCheck(ss[0])) {
System.out.println("這是第 " + (i + 1) + " 個算術式");
for (String s : ss) {
System.out.println(s);
}
saveThreeA++;
}
}
else{
ss = exGen.threeOpGeneratorB();
if (exGen.duplicateCheck(ss[0])) {
System.out.println("這是第 " + (i + 1) + " 個算術式");
for (String s : ss) {
System.out.println(s);
}
saveThreeB++;
}
}
}
System.out.println("一共保留了 " + saveOne + " 個 1 型表達式");
System.out.println("一共保留了 " + saveTwo + " 個 2 型表達式");
System.out.println("一共保留了 " + saveThreeA + " 個 3A 型表達式");
System.out.println("一共保留了 " + saveThreeB + " 個 3B 型表達式");
}
@Test
public void generateExpTest() {
HashMap<String,String> result = exGen.generateExp();
if(result != null) {
System.out.println("最終生成的題目和答案共 " + result.size() + " 對");
for(Map.Entry<String, String> entry: result.entrySet())
{
System.out.println("式子: "+ entry.getKey()+ " 答案: "+entry.getValue());
}
}
else {
System.out.println("range 和 number 不匹配");
}
}
}
UserInterfaceTest 測試用戶接口類
import org.junit.Test;
public class UserInterfaceTest {
@Test
public void mainTest(){
UserInterface.mainTest(new String[] {"-r","2","-n","2000"});
UserInterface.mainTest(new String[] {"-r","1","-n","1"});
UserInterface.mainTest(new String[] {"-r","10","-n","1000"});
UserInterface.mainTest(new String[] {"-e","Exercise.txt","-a","Answer.txt"});
UserInterface.mainTest(new String[] {"-e","ExercisesF.txt","-a","AnswersF.txt"});
}
}
MyAppMasterTest 軟件整體測試,為了檢查代碼覆蓋率
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(value = Suite.class)
@Suite.SuiteClasses(value={
FractionMasterTest.class,
ExerciseGeneratorTest.class,
UserInterfaceTest.class})
public class MyAppMasterTest {
}
整體代碼覆蓋率為** 100% classes, 97% lines **
隨機輸出 10000 條 10 範圍以內的表達式和答案:
點擊我查看10000條表達式
點擊我查看10000條答案
批改給定的題目和答案
點擊查看1000道題目的批改結果
PSP
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
· Estimate | · 估計這個任務需要多少時間 | 30 | 60 |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 45 | 75 |
· Design Spec | · 生成設計文檔 | 60 | 75 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 30 | 60 |
· Coding Standard | · 代碼規範 (為目前的開發制定合適的規範) | 30 | 45 |
· Design | · 具體設計 | 90 | 120 |
· Coding | · 具體編碼 | 660 | 900 |
· Code Review | · 代碼復審 | 60 | 90 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 480 | 630 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 45 | 60 |
· Size Measurement | · 計算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 45 | 40 |
合計 | 1575 | 2175 |
總結
與王翠鸞同學一起討論題目的實現,我主要負責代碼的構思和編寫,王翠鸞同學主要負責我思路的復審以及部分基礎代碼的編寫。在這個作業裏面,王翠鸞同學主要負責了分數類 Fraction 的實現,而我主要負責了表達式生成類 ExerciseGenerator 的實現。結對編程有個好處就是能讓我們能相互審閱對方的代碼,減少出現錯誤的幾率。同時,兩個人商量討論作業的實現,能讓思路更加可靠。本項目中的測試類主要是由我來編寫,由於我在上一次的作業中沒有過多地關註測試的內容,造成了代碼出現了意料之外的bug。這一次,我們對所有的方法使用 junit 進行了單元測試和整體測試,保證了代碼覆蓋到每一行(除了異常),果然發現了很多隱匿的問題。王翠鸞同學的代碼能力還是很紮實的,每次我提出需求,她都能在短時間給我功能完整、註釋完善的代碼反饋,和她合作的過程也十分愉快。
四則運算 Java 實現 劉豐璨,王翠鸞