結對項目


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, …。
  • 運算符:+, −, ×, ÷。
  • 括號:(, )。
  • 等號:=。
  • 分隔符:空格(用於四則運算符和等號前后)。
  • 算術表達式:

 需求:

  1. 控制生成題目的個數
  1. 控制題目中數值(自然數、真分數和真分數分母)的范圍
  1. 生成的題目中計算過程不能產生負數,也就是說算術表達式中如果存在形如e1− e2的子表達式,那么e1≥ e2
  2. 生成的題目中如果存在形如e1÷ e2的子表達式,那么其結果應是真分數
  3. 每道題目中出現的運算符個數不超過3個。
  4. 程序一次運行生成的題目不能重復,即任何兩道題目不能通過有限次交換+和×左右的算術表達式變換為同一道題目。例如,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. 四則運算題目1
  2. 四則運算題目2

……

 

其中真分數在輸入輸出時采用如下格式,真分數五分之三表示為3/5,真分數二又八分之三表示為2’3/8。

  7.在生成題目的同時,計算出所有題目的答案,並存入執行程序的當前目錄下的Answers.txt文件,格式如下:

 

  1. 答案1
  2. 答案2

 

特別的,真分數的運算如下例所示:1/6 + 1/8 = 7/24。

  1. 程序應能支持一萬道題目的生成。
  2. 程序支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計,輸入參數如下:

 

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(); } }

思路

  1. 用隨機數控制運算符的個數
  2. 對表達式隨機插入括號,如果括號涵括了整條表達式,則此條表達式不合格舍去,並重新生成
  3. 中綴轉后綴,后綴求結果,計算過程中,若出現負數,直接舍棄掉,再生成一條

  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;
}
}

思路

  1. 判斷一個result相等的表達式中的所有操作數是否重復,若操作數都相同則舍棄該表達式,“完美”實現查重功能(懶人方法,接地氣)
  2. 文件IO:老一套了,懂的都懂,不懂就學,形而上學,不行退學

 

程序測試截圖:

  1.生成1000條數值在10以內的表達式

  

   

  2.-r參數大於20則會報錯  

  

   3.在Exercises文件中幾道表達式添加答案,其中11,24為正確答案,將Exercises與正確答案Answers對比  

  

  

  4.測試生成一萬條表達式

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM