Github項目地址:https://github.com/HanKiSei/TheLuOfHope
簡介:拿到作業先不急,找好隊友最要緊;動手之前先放松,等着隊友立大功;吃喝玩樂八九天,全靠隊友活神仙;握着鼠標手一抖,項目pull全都有。
結對項目成員:盧楚欽 3118005012 潘毅成 3118005018
PSP:
PSP | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
· Estimate | 估計這個任務需要多少時間 | 20*60 | 24*60 |
Development | 開發 | ||
Analysis | 需求分析 (包括學習新技術) | 150 | 150 |
·Design Spec | 生成設計文檔 | 45 | 60 |
·Design Review | 設計復審 (和同事審核設計文檔) | 60 | 90 |
· Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 60 | 60 |
·Design | 具體設計 | 120 | 120 |
·Coding | 具體編碼 | 480 | 700 |
·Code Review | 代碼復審 | 150 | 180 |
·Test | 測試(自我測試,修改代碼,提交修改) | 240 | 300 |
Reporting | 報告 | ||
·Test Report | 測試報告 | 60 | 60 |
·Size Measurement | 計算工作量 | 30 | 30 |
·Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | ||
合計 |
題目:
- 實現一個自動生成小學四則運算題目的命令行程序(也可以用圖像界面,具有相似功能)。
說明:
- 自然數:0, 1, 2, …。
- 真分數:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 運算符:+, −, ×, ÷。
- 括號:(, )。
- 等號:=。
- 分隔符:空格(用於四則運算符和等號前后)。
- 算術表達式:
需求:
- 控制生成題目的個數
- 控制題目中數值(自然數、真分數和真分數分母)的范圍
- 生成的題目中計算過程不能產生負數,也就是說算術表達式中如果存在形如e1− e2的子表達式,那么e1≥ e2。
- 生成的題目中如果存在形如e1÷ e2的子表達式,那么其結果應是真分數。
- 每道題目中出現的運算符個數不超過3個。
- 程序一次運行生成的題目不能重復,即任何兩道題目不能通過有限次交換+和×左右的算術表達式變換為同一道題目。例如,23 + 45 = 和45 + 23 = 是重復的題目,6 × 8 = 和8 × 6 = 也是重復的題目。3+(2+1)和1+2+3這兩個題目是重復的,由於+是左結合的,1+2+3等價於(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重復的兩道題,因為1+2+3等價於(1+2)+3,而3+2+1等價於(3+2)+1,它們之間不能通過有限次交換變成同一個題目。
生成的題目存入執行程序的當前目錄下的Exercises.txt文件,格式如下:
- 四則運算題目1
- 四則運算題目2
……
其中真分數在輸入輸出時采用如下格式,真分數五分之三表示為3/5,真分數二又八分之三表示為2’3/8。
7.在生成題目的同時,計算出所有題目的答案,並存入執行程序的當前目錄下的Answers.txt文件,格式如下:
- 答案1
- 答案2
特別的,真分數的運算如下例所示:1/6 + 1/8 = 7/24。
- 程序應能支持一萬道題目的生成。
- 程序支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計,輸入參數如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
統計結果輸出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的數字5表示對/錯的題目的數量,括號內的是對/錯題目的編號。為簡單起見,假設輸入的題目都是按照順序編號的符合規范的題目。
遇到的困難及解決方法
困難描述
- 關於隨機生成的帶括號的位置,經過了大量的推演后選擇了懶人方法。為了解決在計算過程中不能出現負數的問題,在走投無路的情況下,也是使用了懶人方法
- 不明白如何根據作業需求,對形如x'y/z的真分數進行計算
- 本人對編程語言的了解不夠,缺乏面向對象的思想,有很多簡便的功能都不會使用,繞了一些大圈子
- 關於對表達式進行查重,使用了set,使用起來有點麻煩
- 在進行乘法運算的時候,有可能出現操作數過大導致int型溢出的問題,所以把range限制為20以內
關鍵代碼or設計說明
用於封裝操作數的Number類
enum Type { // 枚舉實例 NaturalNumber, TrueFraction; } public class Number { /** * 數的類型:<br/> * 1.自然數<br/> * 2.真分數 */ public Type type; /** [整數,分子,分母] */ public int[] value = new int[3]; /** 整數部分 */ public static final byte INTEGRAL_NUMBER_PART = 0; /** 分子部分 */ public static final byte NUMERATOR_PART = 1; /** 分母部分 */ public static final byte DENOMINATOR_PART = 2; @Override public String toString() { if (value[NUMERATOR_PART] == 0) { // 分子為0 return String.valueOf(value[INTEGRAL_NUMBER_PART]); } else if (value[INTEGRAL_NUMBER_PART] == 0) { // 整數部分為0 return String.valueOf(value[NUMERATOR_PART]) + "/" + String.valueOf(value[DENOMINATOR_PART]); } else { // 都不為0 return String.valueOf(value[INTEGRAL_NUMBER_PART]) + "'" + String.valueOf(value[NUMERATOR_PART]) + "/" + String.valueOf(value[DENOMINATOR_PART]); } } /** * 求最大公約數 * * @param n1 * @param n2 * @return */ public static final int gcd(int n1, int n2) { int gcd = 0; if (n1 < n2) {// 交換n1、n2的值 n1 = n1 + n2; n2 = n1 - n2; n1 = n1 - n2; } if (n1 % n2 == 0) { gcd = n2; } while (n1 % n2 > 0) { n1 = n1 % n2; if (n1 < n2) { n1 = n1 + n2; n2 = n1 - n2; n1 = n1 - n2; } if (n1 % n2 == 0) { gcd = n2; } } return gcd; } /** * * @param range 控制生成范圍 */ public Number(int range) { Random r = new Random(); value[INTEGRAL_NUMBER_PART] = r.nextInt(range) + 1; value[NUMERATOR_PART] = r.nextInt(range) + 1; value[DENOMINATOR_PART] = 1 + r.nextInt(range - 1); // 分母小於等於分子,視為自然數 if (value[DENOMINATOR_PART] <= value[NUMERATOR_PART]) { type = Type.NaturalNumber; // 分子,分母分別設置為0和1 value[NUMERATOR_PART] = 0; value[DENOMINATOR_PART] = 1; } else { type = Type.TrueFraction; } } private Number(int numerator, int denominator) throws Exception { if (numerator < 0) { throw new Exception("分子小於0"); } if (denominator <= 0) { throw new Exception("分母小於等於0"); } if (numerator != 0) { int greatestCommonDivisor = gcd(numerator, denominator); numerator /= greatestCommonDivisor; denominator /= greatestCommonDivisor; } value[INTEGRAL_NUMBER_PART] = numerator / denominator; value[NUMERATOR_PART] = numerator % denominator; value[DENOMINATOR_PART] = denominator; // 分子等於0 if (value[NUMERATOR_PART] == 0) { // 分母設置為1 value[DENOMINATOR_PART] = 1; type = Type.NaturalNumber; } else { type = Type.TrueFraction; } } /** * 加法: this + b * * @param b * @return * @throws Exception */ public Number plus(Number b) throws Exception { Number result = new Number(this.value[NUMERATOR_PART] * b.value[DENOMINATOR_PART] + this.value[DENOMINATOR_PART] * b.value[NUMERATOR_PART], this.value[DENOMINATOR_PART] * b.value[DENOMINATOR_PART]); result.value[INTEGRAL_NUMBER_PART] += this.value[INTEGRAL_NUMBER_PART] + b.value[INTEGRAL_NUMBER_PART]; return result; } /** * 減法: this-b * * @param b * @return * @throws Exception */ public Number subtract(Number b) throws Exception { int condition = (this.value[INTEGRAL_NUMBER_PART] * this.value[DENOMINATOR_PART] + this.value[NUMERATOR_PART]) * b.value[DENOMINATOR_PART] - (b.value[INTEGRAL_NUMBER_PART] * b.value[DENOMINATOR_PART] + b.value[NUMERATOR_PART]) * this.value[DENOMINATOR_PART]; if (condition <= 0) { return null; } Number result = new Number( // n1整數乘分母加小數的和,再乘n2分母 - n2整數乘分母加小數的和,再乘n1分母 condition, this.value[DENOMINATOR_PART] * b.value[DENOMINATOR_PART]); return result; } /** * 乘法: this*b * * @param b * @return * @throws Exception */ public Number multiply(Number b) throws Exception { Number result = new Number( (this.value[INTEGRAL_NUMBER_PART] * this.value[DENOMINATOR_PART] + this.value[NUMERATOR_PART]) * (b.value[INTEGRAL_NUMBER_PART] * b.value[DENOMINATOR_PART] + b.value[NUMERATOR_PART]), this.value[DENOMINATOR_PART] * b.value[DENOMINATOR_PART]); return result; } /** * 除法: this/b * * @param b * @return * @throws Exception */ public Number divide(Number b) throws Exception { Number result = new Number((this.value[INTEGRAL_NUMBER_PART] * this.value[DENOMINATOR_PART] + this.value[NUMERATOR_PART]) * b.value[DENOMINATOR_PART], (b.value[INTEGRAL_NUMBER_PART] * b.value[DENOMINATOR_PART] + b.value[NUMERATOR_PART]) * this.value[DENOMINATOR_PART]); return result; } }
生成表達式與計算
public class Expression { private static final Random r = new Random(); private static final char[] operates = new char[] { '+', '-', '×', '÷' }; // 儲存中綴表達式 private LinkedList<Object> infixExpList; // 儲存后綴表達式 private LinkedList<Object> suffixExpList; // 操作數個數 private int count; // 范圍 private int range; // 儲存計算結果 private Number result; // 是否計算過 private boolean isCalculated ; /** * * @param count 操作數個數 * @param range 操作數范圍,[1,range] * @throws Exception */ public Expression(int count, int range) throws Exception { this(count, range, r.nextBoolean()); } /** * * @param count 操作數個數 * @param range 操作數范圍 * @param containParentheses 是否包含括號 * @throws Exception */ public Expression(int count, int range, boolean containParentheses) throws Exception { // Stack不推薦使用,一般用LinkedList作棧 if (count > 4) { throw new Exception("操作數大於4"); } this.infixExpList = new LinkedList<>(); this.count = count; this.range = range; this.isCalculated = false; for (int i = 0 ; i < this.count ; i++) { // 每個操作數前面push一個隨機操作符 if (i != 0) { infixExpList.push(operates[r.nextInt(operates.length)]); } infixExpList.push(new Number(this.range)); } if (containParentheses && this.count > 2) { int j = r.nextInt(2 * this.count - 4); // 左括號的位置應小於2*count-4 // 左括號的位置應該為偶數並且非零 if (j % 2 != 0 && j != 0) { j++; } infixExpList.add(j, '('); infixExpList.add(2 * this.count - r.nextInt(this.count - 1 - j / 2) * 2, ')'); } // 括號不能括整個表達式 if(infixExpList.getFirst().equals('(') && infixExpList.getLast().equals(')')) { infixExpList.removeLast(); infixExpList.removeFirst(); } } /** * 中綴轉后綴 */ public void infixToSuffix() { // 后綴表達式 suffixExpList = new LinkedList<>(); // 操作符棧 LinkedList<Character> s = new LinkedList<>(); int size = infixExpList.size(); for (int i = 0 ; i < size ; i++) { Object e = infixExpList.get(i); // 若e的類是Character,即字符 if (e.getClass() == Character.class) { char tmp; char ch = (Character) e; switch (ch) { case '(': s.push(ch); break; case '+': case '-': while (!s.isEmpty()) { tmp = s.pop(); if (tmp == '(') { s.push('('); break; } suffixExpList.push(tmp); } s.push(ch); break; case '×': case '÷': while (!s.isEmpty()) { tmp = s.pop(); if (tmp == '+' || tmp == '-' || tmp == '(') { s.push(tmp); break; } else { suffixExpList.push(tmp); } } s.push(ch); break; case ')': while (!s.isEmpty()) { tmp = s.pop(); if (tmp == '(') { break; } else { suffixExpList.push(tmp); } } break; }// switch } else { suffixExpList.push(e); } // if } // for while (!s.isEmpty()) { suffixExpList.push(s.pop()); } Collections.reverse(suffixExpList); } /** * 計算后綴 * * @throws Exception */ public void suffixToArithmetic() throws Exception { // 操作數棧 LinkedList<Number> numberStack = new LinkedList<>(); int size = suffixExpList.size(); for (int i = 0 ; i < size ; i++) { Object e = suffixExpList.get(i); if (e.equals('+') || e.equals('-') || e.equals('×') || e.equals('÷')) { // char型 char ch = (Character) e; Number y = numberStack.pop(); Number x = numberStack.pop(); // z = x (操作符) y Number z = null; switch (ch) { case '+': z = x.plus(y); break; case '-': z = x.subtract(y); if (z == null) { result = null; return; } break; case '×': z = x.multiply(y); break; case '÷': z = x.divide(y); break; } numberStack.push(z); } else { // Number類 numberStack.push((Number) e); } } result = numberStack.pop(); } public Number getResult() throws Exception { if (isCalculated) { // 如果計算過一次了 return result; } else { // 轉后綴,同時suffixExpList變成非null infixToSuffix(); // 計算 suffixToArithmetic(); // isCalculated為計算標記,記錄是否計算過 isCalculated = true; // 自然數、真分數、真分數分母 <range if (result != null && (result.value[Number.INTEGRAL_NUMBER_PART] >= range || result.value[Number.DENOMINATOR_PART] >= range) ) { result = null; } return result; } } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (Object instance : infixExpList) { sb.append(instance.toString() + " "); } return sb.append(" = ").toString(); } }
思路:
- 用隨機數控制運算符的個數
- 對表達式隨機插入括號,如果括號涵括了整條表達式,則此條表達式不合格舍去,並重新生成
- 中綴轉后綴,后綴求結果,計算過程中,若出現負數,直接舍棄掉,再生成一條
Main函數與文件IO
public class Main {
private static final Random r = new Random();
private static final int OPERAND_MIN_COUNT = 2;
private static final int OPERAND_MAX_COUNT = 4;
private static final int PROBLEM_DEFAULT_COUNT = 100;
private static final String NUMBER_PATTERN = "^[1-9]{1}\\d*$";
private static final File EXERCISES_FILE = new File("./Exercises.txt");
private static final File ANSWERS_FILE = new File("./Answers.txt");
public static void main(String[] args) throws Exception {
int range = 0, count = 0;
String exerciseFilename = null, answerFilename = null;
for (int i = 0 ; i < (args.length - 1) ; i++) {
String arg = args[i];
switch (arg) {
case "-r":
if (Pattern.matches(NUMBER_PATTERN, args[i + 1])) {
range = Integer.parseInt(args[i + 1]);
}
break;
case "-n":
if (Pattern.matches(NUMBER_PATTERN, args[i + 1])) {
count = Integer.parseInt(args[i + 1]);
}
break;
case "-e":
exerciseFilename = args[i + 1];
break;
case "-a":
answerFilename = args[i + 1];
break;
}
}
// 對答案
if (exerciseFilename != null || answerFilename != null) {
if (exerciseFilename == null) {
System.out.println("缺少-e參數,應提供形如" + EXERCISES_FILE.getName() + "的參數");
}
if (answerFilename == null) {
System.out.println("缺少-a參數應提供形如" + ANSWERS_FILE.getName() + "的參數");
}
checkAnswer(exerciseFilename, answerFilename);
return;
}
if (range == 0) {
System.out.println("-r的參數必須給定且為整數,如-r 10");
return;
}
if (range > 20) {
System.out.println("-r的參數必須小於等於20,否則太為難小學生了");
return;
}
if (count == 0) {
count = PROBLEM_DEFAULT_COUNT;
}
System.out.println("正在生成表達式...");
HashMap<String, HashSet<Expression>> map = new HashMap<>();
for (int i = 0 ; i < count ;) {
Expression exp = new Expression(OPERAND_MIN_COUNT + r.nextInt(1 + OPERAND_MAX_COUNT - OPERAND_MIN_COUNT), range);
Number result = exp.getResult();
// 若計算出一個正確的result
if (result != null ) {
// 若對應result的位置為空
if(map.get(result.toString()) == null) {
map.put(result.toString(), new HashSet<>());
}
Set<Expression> expSet = map.get(result.toString());
// 若expSet沒有這個exp
if(!expSet.contains(exp)) {
expSet.add(exp);
++i;
}
}
}
writeToLocal(map);
System.out.println("已生成" + count + "個表達式及對應答案");
System.out.println("題目文件: " + EXERCISES_FILE.getName());
System.out.println("答案文件: " + ANSWERS_FILE.getName());
}
private static void writeToLocal(HashMap<String, HashSet<Expression>> map) throws Exception {
deleteFile(EXERCISES_FILE, ANSWERS_FILE);
BufferedWriter ebos = new BufferedWriter(new FileWriter(EXERCISES_FILE));
BufferedWriter abos = new BufferedWriter(new FileWriter(ANSWERS_FILE));
int i = 1;
for (String result : map.keySet()) {
Set<Expression> expSet = map.get(result);
for(Expression exp : expSet) {
String exercise = "" + i + ". " + exp.toString();
String answer = "" + i + ". " + result.toString();
ebos.write(exercise);
abos.write(answer);
ebos.newLine();
abos.newLine();
i++;
}
}
// 答案文件寫入完畢,設置只讀
ANSWERS_FILE.setReadOnly();
ebos.close();
abos.close();
}
/**
*
* @param exerciseFilename 當前目錄下的題目文件名(含后綴)
* @param answerFilename 當前目錄下的答案文件名(含后綴)
* @throws FileNotFoundException
*/
private static void checkAnswer(String exerciseFilename, String answerFilename) throws Exception {
File exerciseFile = new File(exerciseFilename);
File answerFile = new File(answerFilename);
if (isExists(exerciseFile, answerFile)) {
BufferedReader ebr = new BufferedReader(new FileReader(exerciseFile));
BufferedReader abr = new BufferedReader(new FileReader(answerFile));
// correct正確的題目數,wrong錯誤的題目數
int correct = 0;
int wrong = 0;
List<String> correctNumberList = new ArrayList<>();
List<String> wrongNumberList = new ArrayList<>();
String exerciseLine;
String answerLine;
while ((exerciseLine = ebr.readLine()) != null && (answerLine = abr.readLine()) != null) {
String exerciseAnswer = exerciseLine.substring(1 + exerciseLine.indexOf('=')).trim();
String realAnswer = answerLine.substring(1 + answerLine.indexOf('.')).trim();
if (exerciseAnswer.equals(realAnswer)) {
++correct;
correctNumberList.add(exerciseLine.substring(0, exerciseLine.indexOf('.')));
} else {
++wrong;
wrongNumberList.add(answerLine.substring(0, answerLine.indexOf('.')));
}
}
System.out.println("Correct: " + correct + " (" + Arrays.toString(correctNumberList.toArray()).replaceAll("\\[|\\]", "") + ")");
System.out.println("Wrong: " + wrong + " (" + Arrays.toString(wrongNumberList.toArray()).replaceAll("\\[|\\]", "") + ")");
ebr.close();
abr.close();
}
}
private static void deleteFile(File... files) {
for (File file : files) {
if (file.exists()) {
file.delete();
}
}
}
private static boolean isExists(File... files) {
for (File file : files) {
if (!file.exists()) {
System.out.println(file.getAbsolutePath() + "不存在");
return false;
}
}
return true;
}
}
思路:
- 判斷一個result相等的表達式中的所有操作數是否重復,若操作數都相同則舍棄該表達式,“完美”實現查重功能(懶人方法,接地氣)
- 文件IO:老一套了,懂的都懂,不懂就學,形而上學,不行退學
程序測試截圖:
1.生成1000條數值在10以內的表達式
2.-r參數大於20則會報錯
3.在Exercises文件中幾道表達式添加答案,其中11,24為正確答案,將Exercises與正確答案Answers對比
4.測試生成一萬條表達式