四則運算題目生成程序


個人作業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             數字,封裝了四則運算和約分等操作
  • Config                    各種配置,比如-n、-r等
  • 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++;
}

測試運行

運行截圖,耗時還是比較長……

生成10000個算式,最大數字10

這里因為÷號不是ascii所以讀取出來的是亂碼,需要加上 -Dfile.encoding=utf-8 參數

檢查算式

算式、答案、成績文件

現在的查重還是做不到檢測交換順序后的重復,只能檢測到數字順序一樣並且運算符和優先級一致的算式,具體來說就是去掉或加上括號的那種重復

PSP

PSP2.1 Personal Software Process Stages Time Predicted Time
Planning 計划 5 5
· Estimate 估計這個任務需要多少時間 5 5
Development 開發 420 973
· Analysis 需求分析 (包括學習新技術) 10 15
· Design Spec 生成設計文檔 - -
· Design Review 設計復審 - -
· Coding Standard 代碼規范 - -
· Design 具體設計 - -
· Coding 具體編碼 200 491
· Code Review 代碼復審 10 270
· Test 測試(自我測試,修改代碼,提交修改) 200 197
Reporting 報告 100 94
. 測試報告 90 94
. 計算工作量 10 -
. 並提出過程改進計划 - -

做這個項目還是花了很多時間的,雖然明明可以把功能完成得差不多就得了,但是為什么還要這么拼呢,對啊為什么呢……
大概是因為完美主義吧,就像我玩游戲一定要做全成就一樣

這個表格的時間我也算得比較嚴格,誤差應該在30分鍾之內

具體編碼這部分雖然一開始就覺得會花很久,結果最后花的時間還是比預期的要多

測試這部分雖然沒有超出估計值,但還是比我之前做項目時花的時間多得多,也是第一次用了單元測試,感覺太棒了,非常好用啊

后來還花了3個半小時來寫文檔和注釋(大部分時間在查單詞- -),不知道該歸類在哪,就寫到代碼復審里了

還有一些步驟我沒做或者不知道哪些屬於它,就沒寫時間了

另外,最煩人的部分是git,帶着各種匪夷所思的錯誤,為了git的一個push花了3個小時,還是在寂寞的半夜

最后寫博客花的時間我沒記,不過加起來也得有3、4個小時吧

小結

這個項目雖然耗費了我大量娛樂時間,但也讓我學到了很多有用的東西

比如JUnit,以前我都懶得寫測試,但現在我第一次體會到了單元測試的好處,再也不用為了測一段代碼就得反復把整個程序跑起來,還各種模擬操作了

再比如javadoc,這是我第一次這么認真地寫javadoc,也基本了解了它的語法和一些表達習慣

還有git,雖然第一次配置配得我頭都快禿了,但配完之后用起來還是很好用的,對代碼管理也是大有幫助


免責聲明!

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



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