個人作業1——四則運算題目生成程式(基于控制台)
項目已送出到Coding.net:arith-exercise
需求分析
- 使用 -n 參數控制生成題目的個數
- 使用 -r 參數控制題目中數值(自然數、真分數和真分數分母)的範圍,該參數可以設定為1或其他自然數。該參數必須給定,否則程式報錯并給出幫助資訊。
- 生成的題目中如果存在形如e1 ÷ e2的子表達式,那麼其結果應是真分數。
- 每道題目中出現的運算符個數不超過3個。
- 其中真分數在輸入輸出時,真分數五分之三表示為3/5,真分數二又八分之三表示為2’3/8。
- 程式一次運作生成的題目不能重複,即任何兩道題目不能通過有限次交換+和×左右的算術表達式變換為同一道題目。
- 程式應能支援一萬道題目的生成 [spoiler]然而并沒有規定時間哈哈哈[/spoiler]。
- 程式支援對給定的題目檔案和答案檔案,判定答案中的對錯并進行數量統計,并會輸出所有題目中重複的題目。
功能設計
- 根據指令行參數種類的不同來分别進入兩種模式:算式生成和算式檢查
- 設計一個數字類,用來封裝分數和四則運算操作
- 題目沒有給出算式生成模式時輸出的答案檔案名的指令行參數,是以預設為“Answer.txt”
設計實作
采用Java開發
項目結構
- arith
-
封裝了數學方法,比如算式校驗和算式計算Arith
-
檢查測驗檔案,進行算式查重,并将答案與答案檔案裡的比對,輸出結果到成績檔案Checker
-
生成算式Creator
-
- model
-
表達式Expression
-
數字,封裝了四則運算和約分等操作Number
-
-
各種配置,比如-n、-r等Config
-
程式入口,讀取參數并進入相應模式Main
Model類
這裡列出兩個model類——數字和表達式,因為篇幅原因省略掉了很多方法和方法的實作,具體可以到Coding.net裡檢視
public class Number {
// 把每個數字都視為分數(可能是假分數), 如果是整數的話就把分母設為1
private int mNumerator;
private int mDenominator;
// 通過整數、分子和分母、格式化的數字字元串三種方式來構造
public Number(int value) { }
public Number(int numerator, int denominator) { }
public Number(String number) { }
// 根據運算符來執行對應四則運算
public Number operate(String operator, Number number) { }
// 封裝的四則運算操作
public Number add(Number number) { }
public Number sub(Number number) { }
public Number mul(Number number) { }
public Number div(Number number) { }
// 約分分數,用到了網上找到的最大公約數算法
// 每次運算後都會調用一次該函數
public void reduce() {
if (mNumerator == 0) // 不需要約分
return;
// 計算最大公約數
// Ref: http://blog.csdn.net/iwm_next/article/details/7450424
int a = Math.abs(mNumerator);
int b = mDenominator;
while (a != b)
if (a > b)
a = a - b;
else
b = b - a;
mNumerator /= a;
mDenominator /= a;
}
}
// 表達式是一個字元串數組,其中每個元素都是格式化的數字或者運算符,比如
// (1+2/3)*4'5/6 => ["(", "1", "+", "2/3", ")", "*", "4'5/6"]
public class Expression extends ArrayList<String> {
// 解析一個字元串,轉化為表達式類型
public static Expression fromString(String src) { }
}
代碼說明
對于表達式的計算,參考了這篇部落格的算法,但是他的算法有個地方有點問題:
else { // 優先級小于棧頂運算符,則彈出
tmp = stack.pop();
// 這裡不應該把element加入suffix裡,應該把它壓入棧中
// suffix.append(tmp).append(" ").append(element).append(" ");
suffix.append(tmp).append(" ");
stack.push(element);
}
生成算式的方法
public static void create(Config config, boolean putAnswer) {
if (!isConfigValid(config))
return;
int number = config.number;
int range = config.range;
List<Expression> expressions = new ArrayList<>(number);
List<Number> answers = new ArrayList<>(number);
for (int i = 0; i < number; i++) {
try {
Expression exp = createExpression(range);
Number ans = Arith.evaluate(exp);
// 檢查是否存在重複的算式,先檢查答案是否重複再檢查算式本身
if (Checker.hasSameAnswer(answers, ans) && Checker.findDuplicate(expressions, exp) != -1)
throw new ArithmeticException("Expression duplicated: " + exp);
expressions.add(exp);
answers.add(ans);
} catch (ArithmeticException e) {
// 如果生成失敗了就回退,再試一次
i--;
}
}
// 儲存結果到檔案
output(config.output, expressions, answers, putAnswer);
}
生成随機數字的方法
private static Number randomNumber(int numberMax) {
if (Math.random() < PR_INTEGER) // PR_INTEGER=0.8,是生成一個整數的機率
// 生成一個整數
return new Number((int) (Math.random() * numberMax - 1) + 1);
else
// 生成一個分數
return new Number((int) (Math.random() * numberMax - 1) + 1, (int) (Math.random() * numberMax - 1) + 1);
}
在原算式的基礎上添加一個運算的方法,随機選取一個數字,比如将1+2*3裡的2替換為2-4,或者帶括号的(2-4)
private static void addOperation(Expression exp, int numberMax) {
int size = exp.size();
int position = 0;
int loops = 0;
// 随機選擇一個數字
while (true) {
if (Arith.isNum(exp.get(position).charAt(0)) && Math.random() > 0.5)
break;
// 如果搜尋結束了還沒有選中數字,就回退到起點然後重新搜尋
if (++position == size)
position = 0;
if (loops++ == 50) {
System.out.println("Oops, something went wrong...?");
return;
}
}
boolean addBrackets = Math.random() > PR_BRACKET; // PR_BRACKET=0.5,是插入一個括号的機率
if (addBrackets)
exp.add(position++, "(");
exp.add(++position, randomOperator());
exp.add(++position, randomNumber(numberMax).toString());
if (addBrackets)
exp.add(++position, ")");
}
從檔案讀取并檢查算式結果的算法
// 示例:
// Exercise.txt > 1. 1+2*3=7
// Answer.txt > 1. 7
while ((expLine = exerciseReader.readLine()) != null) {
separator = expLine.indexOf('=');
exp = Expression.fromString(expLine.substring(expLine.indexOf('.') + 2, separator));
ansLine = answerReader.readLine();
rightAnsStr = ansLine.substring(ansLine.indexOf('.') + 2);
// 檢查答案
if (separator + 1 < expLine.length() // 填有答案
&& rightAnsStr.equals(expLine.substring(separator + 1))) // 等于正确答案
corrects.add("" + index);
else
wrongs.add("" + index);
// 檢查重複
rightAns = new Number(rightAnsStr);
if (hasSameAnswer(rightAnswers, rightAns) && (duplicate = findDuplicate(expressions, exp)) != -1)
// 暫存重複的兩個表達式的下标和對象
repeats.add(new ExpressionPair(duplicate + 1, index, expressions.get(duplicate), exp));
expressions.add(exp);
rightAnswers.add(rightAns);
index++;
}
測試運作
運作截圖,耗時還是比較長……

這裡因為÷号不是ascii是以讀取出來的是亂碼,需要加上
-Dfile.encoding=utf-8
參數
算式、答案、成績檔案
現在的查重還是做不到檢測交換順序後的重複,隻能檢測到數字順序一樣并且運算符和優先級一緻的算式,具體來說就是去掉或加上括号的那種重複
PSP
PSP2.1 | Personal Software Process Stages | Time Predicted | Time |
---|---|---|---|
Planning | 計劃 | 5 | |
· Estimate | 估計這個任務需要多少時間 | ||
Development | 開發 | 420 | 973 |
· Analysis | 需求分析 (包括學習新技術) | 10 | 15 |
· Design Spec | 生成設計文檔 | - | |
· Design Review | 設計複審 | ||
· Coding Standard | 代碼規範 | ||
· Design | 具體設計 | ||
· Coding | 具體編碼 | 200 | 491 |
· Code Review | 代碼複審 | 270 | |
· Test | 測試(自我測試,修改代碼,送出修改) | 197 | |
Reporting | 報告 | 100 | 94 |
. | 測試報告 | 90 | |
計算工作量 | |||
并提出過程改進計劃 |
做這個項目還是花了很多時間的,雖然明明可以把功能完成得差不多就得了,但是為什麼還要這麼拼呢,對啊為什麼呢……
大概是因為完美主義吧,就像我玩遊戲一定要做全成就一樣
這個表格的時間我也算得比較嚴格,誤差應該在30分鐘之内
具體編碼這部分雖然一開始就覺得會花很久,結果最後花的時間還是比預期的要多
測試這部分雖然沒有超出估計值,但還是比我之前做項目時花的時間多得多,也是第一次用了單元測試,感覺太棒了,非常好用啊
後來還花了3個半小時來寫文檔和注釋(大部分時間在查單詞- -),不知道該歸類在哪,就寫到代碼複審裡了
還有一些步驟我沒做或者不知道哪些屬于它,就沒寫時間了
另外,最煩人的部分是git,帶着各種匪夷所思的錯誤,為了git的一個push花了3個小時,還是在寂寞的半夜
最後寫部落格花的時間我沒記,不過加起來也得有3、4個小時吧
小結
這個項目雖然耗費了我大量娛樂時間,但也讓我學到了很多有用的東西
比如JUnit,以前我都懶得寫測試,但現在我第一次體會到了單元測試的好處,再也不用為了測一段代碼就得反複把整個程式跑起來,還各種模拟操作了
再比如javadoc,這是我第一次這麼認真地寫javadoc,也基本了解了它的文法和一些表達習慣
還有git,雖然第一次配置配得我頭都快秃了,但配完之後用起來還是很好用的,對代碼管理也是大有幫助