天天看點

四則運算題目生成程式

個人作業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++;
}
           

測試運作

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

四則運算題目生成程式

這裡因為÷号不是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,雖然第一次配置配得我頭都快秃了,但配完之後用起來還是很好用的,對代碼管理也是大有幫助