結對程式設計--自動生成小學四則運算

翻滚的车厘子發表於2024-03-26

小學四則運算題目生成

這個作業屬於哪個課程 軟體工程2024
這個作業要求在哪裡 結對專案
這個作業的目標 完成結對專案,共同合作實現自動生成小學四則運算題目
參與人員 溫澤坤3122004582、黃浩3122004571

作業github地址


PSP2.1表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 60 60
· Estimate · 估計這個任務需要多少時間 60 60
Development 開發 620 1050
· Analysis · 需求分析 (包括學習新技術) 120 180
· Design Spec · 生成設計文件 40 30
· Design Review · 設計複審 30 10
· Coding Standard · 程式碼規範 (為目前的開發制定合適的規範) 10 20
· Design · 具體設計 30 30
· Coding · 具體編碼 300 600
· Code Review · 程式碼複審 30 60
· Test · 測試(自我測試,修改程式碼,提交修改) 60 120
Reporting 報告 120 120
· Test Repor · 測試報告 60 60
· Size Measurement · 計算工作量 30 30
· Postmortem & Process Improvement Plan · 事後總結, 並提出過程改進計劃 30 30
· 合計 800 1230

一、需求分析


設計一個能夠批次生成小學四則運算的程式,要求滿足以下標準:

1、使用 -n 引數控制生成題目的個數。

分析:題目數量可以隨引數改變而改變

2、使用 -r 引數控制題目中數值。

分析:題目數值的最大值可以隨引數改變而改變

3、生成的題目中計算過程不能產生負數。

分析:要對減法進行額外判斷處理

4、生成的題目中如果存在形如e1÷ e2的子表示式,那麼其結果應是真分數。

分析:要對結果進行轉化,使其滿足真分數形式

5、每道題目中出現的運算子個數不超過3個。

分析:限制運算子數量和運算數數量,使前者最多不超過3個,後者最多不超過4個

6、程式一次執行生成的題目不能重複。

分析:判斷題目是否重複,若重複,則不生成

7、在生成題目的同時,計算出所有題目的答案,並存入執行程式的當前目錄下的Answers.txt檔案。

分析:對生成的題目進行計算,結果存入指定檔案

8、程式應能支援一萬道題目的生成。

分析:程式生成一萬道題目也不會出錯

9、程式支援對給定的題目檔案和答案檔案,判定答案中的對錯並進行數量統計。

分析:額外設定一個檢查統計答案的模組,對輸入的題目檔案計算答案,與給定的答案檔案比對,將統計結果存進指定檔案

二、設計實現過程 :

1. 定義兩個抽象類:

  1. Fraction類:真分數類,實現如何存放一個真分數,該類包含了實現加減乘除的計算與字串的轉換的函式;
  2. Question類:問題組成類,將問題拆分為編號,算術表示式,答案三個部分組成;
    包含了返回答案的字串形式和返回表示式的字串形式的函式;

2. 六個相關的函式模組:

  1. main模組:設定main函式透過命令列設定引數,函式接受到引數後首先判斷引數個數是否符合要求;
    然後根據"-n","-r","-e","-a"四個標誌來獲取需要的引數,再判斷引數有無結對傳入
    以及傳入的引數是否滿足構成呼叫generateQuestions或check()函式的要求,若滿足則呼叫
    不滿足要求則報錯;

  2. GenerateQuestion模組:設定按數量與數值範圍的要求生成題目集的函式generateQuestions(),定義
    Question類集合獲取生成的題目集,利用迴圈呼叫generateExpression()函式
    來生成表示式,並呼叫evaluate()函式解析表示式生成答案,最後將序號,表示式、
    答案包裝成Question類物件新增進集合裡,從而得到問題集,並呼叫;

  3. GenerateExpression模組:主要函式為生成表示式generateExpression()函式,該函式呼叫generateSimpleExpression()
    生成簡單表示式,呼叫generateComplexExpression()生成複雜表示式,且設定隨機呼叫;而這兩個函式
    又會呼叫生成整數generateOperand()函式與生成真分數generateTrueFraction()函式來隨機生成整數和真分數
    以構造出隨機算術表示式,最終透過generateExpression()函式返回算術表示式的字串形式;

  4. ExpressionParsing模組:主要函式為evaluate()函式,透過設定運算元棧和運算子棧,對傳進來的表示式進行一個個的讀取入棧;
    當字元為數字型別時入運算元棧,當字元為運算子型別時(透過isOperator()函式判斷)入運算子棧;隨後
    透過判斷運算子優先順序(透過hasPrecedence()函式以及有無括號"()"來判斷)計算出答案,該過程呼叫applyOp()
    函式實現,且整個計算過程是一邊入棧一邊計算的,當滿足計算要求時計算結果,然後將結果入棧,後續繼續計算直到結束;
    最後返回的是一個真分數類變數,該變數作為答案後續可進行自動字串轉換;

  5. CorrectExpression模組:設定check()函式獲取兩個檔案路徑引數,透過BufferedReader類讀取文字內容,在函式中利用迴圈的單行讀取來獲取兩個檔案
    相同序號題目的答案字串,然後判斷兩個字串大小是否相同,相同則代表答案一樣,對correct字串變數進行修改,不相同則
    對wrong字串變數進行修改,最終得到兩個存放正確編號和錯誤編號的字串變數,並將其列印進指定檔案;

  6. Save模組:內含saveQuestionsToFile()函式透過迴圈來列印問題到問題檔案中去,saveAnswerToFile()函式透過迴圈列印答案到答案檔案;

三、 展示關鍵程式碼

  1. 用於生成算術表示式的GenerateExpression模組:
package main;

import java.util.Random;

public class GenerateExpression {
    private static final String[] OPERATORS = {"+", "-", "×", "÷"};
    private static final int MIN_OPERAND = 1;   // 假設運算元的最小值為1
    private static final int MIN_EXPRESSION_PARTS = 2; // 表示式至少包含兩部分(兩個運算元和一個運算子)
    private static final Random random=new Random();

    public static String generateExpression(int minParts, int maxParts,int MaxNumber) {
    //隨機生成算術表示式函式
        if (minParts > maxParts || minParts < MIN_EXPRESSION_PARTS) {
            throw new IllegalArgumentException("Invalid range for expression parts");
        }

        int parts = minParts + random.nextInt(maxParts - minParts + 1);

        if (parts<3) {
            return generateSimpleExpression(parts,MaxNumber);
        } else {
            return generateComplexExpression(parts,MaxNumber);
        }
    }

    private static String generateSimpleExpression(int parts,int MaxNumber) {
    //生成不帶括號的簡單算術表示式
        StringBuilder sb = new StringBuilder();
        if(parts==1){
            if(random.nextBoolean()) sb.append(generateTrueFraction(MaxNumber));
            else sb.append(generateOperand(MaxNumber));
            sb.append(' ');
            return sb.toString();
        }
        //int flat=0;減號的嘗試處理
        for (int i = 0; i < parts - 1; i++) {
            if(random.nextInt(5)==1) sb.append(generateTrueFraction(MaxNumber));
            else sb.append(generateOperand(MaxNumber));
            sb.append(' ');
            String Op;
            Op=OPERATORS[random.nextInt(OPERATORS.length)];
            //if(Op=="-")flat++;如果有減號,flat++
            sb.append(Op);
            sb.append(' ');
        }
        if(random.nextInt(5)==1) sb.append(generateTrueFraction(MaxNumber));
        else sb.append(generateOperand(MaxNumber));
        //嘗試對減號進行處理,但沒有達到相要的效果
        /*if(flat!=0 ) {
            String str = sb.toString();
            String[] calculate = str.split("-");
            Fraction f1 = ExpressionParsing.evaluate(calculate[0]);
            Fraction f2 = ExpressionParsing.evaluate(calculate[1]);
            Fraction f3 = f1.divide(f2);
            if (f3.numerator < f3.denominator) return generateSimpleExpression(parts, MaxNumber);
        }/* else if (calculate.length == 3) {
                f1 = f1.subtract(f2);
                f2 = ExpressionParsing.evaluate(calculate[2]);
                f3 = f1.divide(f2);
                if (f3.numerator < f3.denominator) return generateSimpleExpression(parts, MaxNumber);
            }
        }
            /*else if(calculate.length==4){
                f1=f1.subtract(f2);
                f2=ExpressionParsing.evaluate(calculate[2]);
                f3=f1.divide(f2);
                if(f3.numerator<f3.denominator)return  generateSimpleExpression(parts,MaxNumber);
                else{
                    f1=f1.subtract(f2);
                    f2=ExpressionParsing.evaluate(calculate[3]);
                    f3=f1.divide(f2);
                    if(f3.numerator<f3.denominator)return  generateSimpleExpression(parts,MaxNumber);
                }
            }
        }*/
        return sb.toString();
    }

    private static String generateComplexExpression(int parts, int MaxNumber) {
    //生成帶括號的複雜算術表示式
        if (random.nextBoolean() ) {
            return generateSimpleExpression(parts,MaxNumber);
        }
        int subParts = random.nextInt(parts-1)+1;
        String left = generateSimpleExpression(subParts,MaxNumber);
        String right = generateSimpleExpression(parts - subParts,MaxNumber);
        if(subParts==1||parts-subParts==1)
            return subParts==1?left+OPERATORS[random.nextInt(OPERATORS.length)] + "(" + right + ")":
                    "(" + left +")" + OPERATORS[random.nextInt(OPERATORS.length)] +right;
        return "(" + left +")" + OPERATORS[random.nextInt(OPERATORS.length)] + "(" + right + ")";
    }
    //隨機生成整數的函式
    private static int generateOperand(int MAX_OPERAND) {
        return MIN_OPERAND + random.nextInt(MAX_OPERAND - MIN_OPERAND + 1);
    }
    //隨機生成真分數的函式
    public static String generateTrueFraction(int MAX_OPERAND){
        int denominator=random.nextInt(9)+1;//分母不等於0;
        int numerator=random.nextInt(9)+1;
        while (numerator >= denominator) {
            numerator = random.nextInt(9) + 1;
            denominator = random.nextInt(9) + 1;
        }
        int wholePart=random.nextInt(MAX_OPERAND);
        if(wholePart==0)return numerator+"/"+denominator;
        return wholePart+"'"+numerator+"/"+denominator;
    }

}
  1. 用於計算答案的ExpressionParsing模組:
package main;

import java.util.Stack;

public class ExpressionParsing {
    // 計算表示式的值
    public static Fraction evaluate(String expression) {
        Stack<Character> operators = new Stack<>();
        Stack<Fraction> value = new Stack<>();
        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);

            if (Character.isDigit(c)) {
                // 處理多位數,例如"123"
                int number = 0;
                while (i < expression.length() && Character.isDigit(expression.charAt(i))) {
                    number = number * 10 + (expression.charAt(i) - '0');
                    i++;
                }
                int denominator=1;
                int numerator=number;
                //真分數處理,’後面是形如8/9的形式,共佔據4個字元
                if(i < expression.length() && expression.charAt(i)=='\''){
                    i++;
                    numerator = (expression.charAt(i++)-'0');
                    i++;
                    denominator = (expression.charAt(i++)-'0');
                    numerator+=number*denominator;
                }
                else if(i < expression.length() && expression.charAt(i)=='/'){
                    i++;
                    denominator= (expression.charAt(i++)-'0');
                }
                value.push(new Fraction(numerator,denominator));
                i--; // 因為for迴圈也會增加i,所以需要減一以避免跳過字元
            } else if(c==' '||c=='=') {
            }//遇到空格符救跳過;
            else if (c == '(') {
                operators.push(c);
            } else if (c == ')') {
                // 計算括號內的表示式
                while (!operators.isEmpty() && operators.peek() != '(') {
                    value.push(applyOp(operators.pop(), value.pop(), value.pop()));
                }
                // 彈出左括號
                if (!operators.isEmpty()) {
                    operators.pop();
                }
            } else if (isOperator(c)) {
                while (!operators.isEmpty() &&operators.peek()!='('&& hasPrecedence(c, operators.peek())) {
                    value.push(applyOp(operators.pop(), value.pop(), value.pop()));
                }
                operators.push(c);
            }
        }

        // 處理剩餘的運算子
        while (!operators.isEmpty()) {
            value.push(applyOp(operators.pop(), value.pop(), value.pop()));
        }
        return value.pop();
    }

    private static boolean isOperator(char c) {
        return c == '+' || c == '-' || c == '×' || c == '÷';
    }

    private static Fraction applyOp(char op, Fraction f1, Fraction f2) {
        switch (op) {
            case '+': return f1.add(f2);
            case '-': return f2.subtract(f1);
            case '×': return f1.multiply(f2);
            case '÷': return f2.divide(f1); //
            default:return new Fraction(1,1);
        }
    }

    private static boolean hasPrecedence(char op1, char op2) {//op1的優先順序更低或相等時返回true;
        return (op1 != '×' && op1 != '÷') || (op2 != '+' && op2 != '-');
    }
}
  1. 真分數類的定義Fraction模組
package main;

public class Fraction {
    public int numerator;  // 分子
    public int denominator;  // 分母

    // 建構函式
    public Fraction(int numerator, int denominator) {
        if (denominator == 0) {
            throw new IllegalArgumentException("Denominator cannot be zero");
        }
        //if (numerator < 0 || denominator < 0) {
            //throw new IllegalArgumentException("Both numerator and denominator must be positive");
        //}
        this.numerator = numerator;
        this.denominator = denominator;
        reduce();
    }

    // 約分
    private void reduce() {
        int gcd = gcd(numerator, denominator);
        numerator /= gcd;
        denominator /= gcd;
    }

    // 計算最大公約數
    private int gcd(int a, int b) {
        if (b == 0) {
            return a;
        }
        return gcd(b, a % b);
    }

    // 加法
    public Fraction add(Fraction other) {
        int newNumerator = this.numerator * other.denominator + this.denominator * other.numerator;
        int newDenominator = this.denominator * other.denominator;
        return new Fraction(newNumerator, newDenominator);
    }

    // 減法
    public Fraction subtract(Fraction other) {
        int newNumerator = this.numerator * other.denominator - this.denominator * other.numerator;
        int newDenominator = this.denominator * other.denominator;
        return new Fraction(newNumerator, newDenominator);
    }

    // 乘法
    public Fraction multiply(Fraction other) {
        int newNumerator = this.numerator * other.numerator;
        int newDenominator = this.denominator * other.denominator;
        return new Fraction(newNumerator, newDenominator);
    }

    // 除法
    public Fraction divide(Fraction other) {
        if (other.numerator == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        int newNumerator = this.numerator * other.denominator;
        int newDenominator = this.denominator * other.numerator;
        return new Fraction(newNumerator, newDenominator);
    }

    // 轉換為字串
    @Override
    public String toString() {
        if(numerator==0)return numerator+"";
        if(Math.abs(numerator)>=Math.abs(denominator)){
            if(denominator==1)return numerator+"";
            else if(denominator==-1)return -numerator+"";
            else
            {
                int whole=numerator/denominator;
                return whole+"'"+(numerator-whole*denominator)+"/"+denominator;
            }
        }

        return numerator + "/" + denominator;
    }

}

四、 測試程式碼模組:

  1. main模組:
package main;

import org.junit.jupiter.api.Test;

import java.io.IOException;

class mainTest {
    @Test
    public void TestMain() throws IOException {
        //隨機生成題目測試
        String[] args={"-n10","-r10"};
        main.main(args);
        System.out.println("------------------");


        //判斷對錯
        String[] args1={"-eD:\\Exercises.txt","-aD:\\Answers.txt"};
        main.main(args1);
        System.out.println("------------------");


        //全部正確傳入
        String[] args2={"-n10","-r10","-eD:\\Exercises.txt","-aD:\\Answers.txt"};
        main.main(args2);
        System.out.println("------------------");


        //錯誤輸入
        String[] args3={"-n10"};
        main.main(args3);
        System.out.println("------------------");


        //不成對出現正確引數
        String[] args4={"-n10","-eD:\\Exercises.txt"};
        main.main(args4);
        System.out.println("-------------------");


        //輸入三個引數
        String[] args5={"-r10","-n10","-eD:\\Exercises.txt"};
        main.main(args5);
        System.out.println("-------------------");
    }
}
  1. GenerateQuestion模組:
package main;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class GenerateQuestionTest {
    @Test
    public void generateTest(){
        int[] numberOfQuestions={10,100,1000,10000};
        int[] maxNumber={1,10,100};
        //10道題生成
        List<Question> questions1=GenerateQuestion.generateQuestions(numberOfQuestions[0],maxNumber[0]);
        System.out.println(questions1);
        System.out.println("--------------");


        //100道題生成
        List<Question> questions2=GenerateQuestion.generateQuestions(numberOfQuestions[1],maxNumber[2]);
        System.out.println(questions2);
        System.out.println("--------------");

        //1000道題
        List<Question> questions3=GenerateQuestion.generateQuestions(numberOfQuestions[2],maxNumber[1]);
        System.out.println(questions3);
        System.out.println("--------------");


        //10000道題生成
        List<Question> questions4=GenerateQuestion.generateQuestions(numberOfQuestions[3],maxNumber[1]);
        System.out.println(questions1);
        System.out.println("--------------");
    }

}
  1. ExpressionParsing模組:
package main;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ExpressionParsingTest {
    @Test
    public void evaluateTest(){
        String[] args={"1+7=","(13+5)-8=","3×4+7'5/4=","3÷4+5'1/4"};
        //簡單加法測試
        Fraction test1=ExpressionParsing.evaluate(args[0]);
        System.out.println(test1.toString());
        System.out.println("-------------------");

        //複雜加減法測試
        Fraction test2=ExpressionParsing.evaluate(args[1]);
        System.out.println(test2.toString());
        System.out.println("-------------------");


        //引入真分數以及乘法測試
        Fraction test3=ExpressionParsing.evaluate(args[2]);
        System.out.println(test3.toString());
        System.out.println("-------------------");


        //引入真分數以及除法測試
        Fraction test4=ExpressionParsing.evaluate(args[3]);
        System.out.println(test4.toString());
        System.out.println("-------------------");
    }

}

五、測試報告:

  1. 程式碼覆蓋率:

  2. 測試結果:main模組以及ExpressionParsing模組的測試正確執行,沒有報錯,但在GenerateQuestion模組測試時,由於沒有
    保證算數解析時計算過程出現分母不為0,所以在測試題目數量巨大時計算過程分母出現0的機率增大導致測試失敗,所以不能正確執行;

  3. 部分題目以及答案生成的結果:



    結果:


六、 效能分析:

  1. 由於未能及時匯入Jprofile外掛,暫時不能由軟體實現自動分析;
  2. 個人意見程式主要耗時在ExpressionParsing()即表示式解析函式以及GenerateExpression()即生成表示式模組上,
    且其中表示式解析函式由於開拓棧記憶體佔用記憶體較多,另外GenerateQuestions()即生成相應要求的題目數量上佔用記憶體也較多

七、程式碼存在的不足:

  1. 做出多種嘗試但還是未能保證隨機生成題目時的計算過程中出現除法的被除數是0,且仍有可能計算過程產生負數;
  2. 未能保證題目完全不重複的情況,雖然重複機率極低

八、總結

浩:

總結成敗得失,分享經驗教訓

這次結對專案,我們基本上完成了題目的要求,主要不足之處可能是有些地方還需改進。不過我認為這次專案整體來說是成功的,
在完成過程中,我們學到了很多新知識,對java的掌握也更熟練,最重要的是,我們在解決一個難題時,進行了思想上火花的碰撞。
我們提出自己的觀點,傾聽理解對方的觀點,表述自己的看法,共同思考解決方案,最終一個個問題迎刃而解。其中,我們獲得了寶貴的經驗,
比如說:針對一個模組的實現,先各自提供自己的思路,然後分析可行性,比較不同方法的效率;又或者說,在執行時,
透過逐一控制呼叫的模組,來檢查程式問題所在;也獲得了教訓,比如說異常處理不夠到位,導致程式列印亂碼等等。

結對感受:

第一次與他人共同完成一個專案,覺得頗有成就感,並且在合作過程中思想的碰撞讓我覺得受益匪淺。對方在一些問題上有著獨特的見解,
而且對一些細節也能考慮到位,讓我深感佩服,特別是能夠仔細傾聽和分析我提出的猜想。結對專案最重要的就是兩個人及時的溝通和默契的配合,
經過這次專案,我對合作完成開發軟體積累了寶貴的經驗。


坤:

總結成敗得失,分享經驗教訓

*優點:
二人合作提高了工作效率,減少了工作量;
相互監督督促,彌補了個人不足;
透過交流學習,提高了程式設計水平;
*缺點:
在合作完成專案時,偶爾會有理解分歧以及意見分歧從而消耗了一定時間
由於都是Java和git新手,在分工方面沒有明確的準則,都是個人想到什麼做什麼,比較雜亂;

結對感受:

第一次與他人合作完成專案,過程雖跌宕起伏,但總體還是很不錯的,特別享受與搭檔溝通交流解決問題的過程,
對雙方都受益良多,當成功完成程式的時候頗有成就感,程式設計過程出現有不足的地方,我們都一起不斷的嘗試過解決,
雖然有未能解決的地方,但總體來說還是滿意的,總的來說這次合作我們都盡力了,貢獻出了我們對這個專案所瞭解的一切,
相互彌補不足,擴大優點,受益匪淺。

相關文章