天天看點

2017202110105-進階軟體工程2017第2次作業

GitHub位址

https://github.com/setezzy/MyCalculator

PSP

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 10
·Estimate · 預估時間
Development 開發 570 930
·Analysis · 需求分析(包括學習新技術) 30 40
·Design Spec · 生成設計文檔 20
·Design Review · 設計複審
·Coding Standard · 代碼規範
·Design · 具體設計 60 100
·Coding · 具體編碼 240 480
·Code Review · 代碼複審
·Test · 測試(自我測試,修改代碼,送出修改) 120 180
Reporting 報告 150
·Test Report · 測試報告
·Size Measurement · 計算工作量
·Postmortem&Process Improvement Plan · 事後總結并提出過程改進計劃
合計 730 1090

需求分析

目标

本軟體的主要目标為:四則混合運算題目生成及計算

使用者特點

本軟體最終使用者為國小生(或國小教師),會基本的四則混合運算,不會負數計算。故我們應基于上述使用者特點進行軟體開發。

假定和限制

開發方法:面向對象的開發技術

開發語言:Java

開發期限:1周

需求規定

由于軟體規模較小,在此隻考慮功能需求。以國小生為主要服務對象,對軟體的功能初步設定為:

a.表達式生成功能。分為操作數随機生成,運算符随機生成以及最終表達式生成。操作數包括100以内正整數和真分數;運算符包括( +, -, *, ÷);最終表達式需要包含帶括号的子表達式。其中操作數及運算符序列個數也随機生成。

b. 表達式計算功能。要求計算帶括号的混合運算的結果,若最後結果為分數,則需要對分數進行化簡。

c. 使用者互動功能。首先判斷使用者輸入是否非法,例如輸入字母時要求使用者重新輸入;再對使用者答案進行正确性判斷:若正确,記得分,若錯誤,顯示正确答案。最後顯示使用者的最終得分。

d. 用指令行參數n控制題目數量。

功能圖

2017202110105-進階軟體工程2017第2次作業

運作環境規定

裝置:PC

支援:PC支援Windows, Linux等作業系統

控制:本軟體主要通過cmd運作,輸入指令即可控制軟體,無特殊要求。

詳細設計

設計思路

a. 生成操作數數組:随機生成正整數或真分數,可由随機數決定。特别注意的是生成分數時,分子應小于分母。将操作數數目作為該方法變量,循環生成操作數,存入數組。

b. 生成運算符數組:随機生成四種運算符,通過switch()實作。當生成的運算符為乘或除時,考慮插入括号,且保證括号插入在低優先級運算符兩側:若乘/除号前一個是加/減号,可以插入括号,是否插入可由随機數決定。将操作數數目作為該方法變量,循環生成操作數,存入數組,當運算符數量達到最大值時,最後插入”=”。

c. 生成表達式:将a,b步驟生成的序列按序插入就得到一個符合邏輯的表達式。首先周遊操作數數組,當有括号時,判斷是否為左括号,若是,則先插入括号,再依次插入數字和運算符和對應的右括号。

d. 表達式計算:在這裡并未采用逆波蘭表達式計算方法,而是參考了其他實作方法:先定義無括号的方法,具體實作為将表達式拆分分别放入list(分為操作數list和運算符list),接下來取運算符,取對應索引處左右的操作數,計算,并将計算結果放回目前索引位置。有括号的方法,具體實作為查找表達式中的括号,取出括号内的子表達式,調用無括号方法進行計算,用該計算結果替換該子串,得到新的表達式,再遞歸調用有括号方法。

e. 使用者互動:用main()函數的String args[]參數接收使用者定義的題目數量,循環調用c步驟的方法生成題目;接收使用者輸入的答案,若不合法則要求重新輸入,将使用者答案與d步驟得到的運算結果比對,若相等,則顯示“正确”,并計入得分,否則顯示“錯誤”并顯示正确答案。

設計實作

程式整體流程圖

2017202110105-進階軟體工程2017第2次作業

類圖

2017202110105-進階軟體工程2017第2次作業

核心代碼說明

CreateFig類

考慮到需要生成真分數,我定義了一個對象數組,友善将分子(ele)、分母(den)分開存儲。整數可表示為ele/1的形式,分數表示為ele/den形式。

public class CreateFig {
    public define[] fig (int count) { //定義生成操作數的數量
        int flag; //是否生成分數
        define[] num = new define[10];
        define def;
        for (int i = 0; i <= count; i++) {
            def = new define();
            flag = (int) (Math.random() * 100) % 3;
            if (flag == 0) {
                def.setDen((int) (Math.random() * 18) + 2); //分母為[2,20)内随機數
                def.setEle((int) (Math.random() * def.getDen()) + 1); //分子要小于分母
                num[i] = def;
            } else {
                def.setDen(1);
                def.setEle((int) (Math.random() * 19) + 1); //生成[1,20)範圍内的整數
            }
        }
            return num;
        }
}
           

Calculate類

方法主要參考 HeadingAlong的部落格 。将表達式的運算符和操作數分别存入兩個list中,周遊運算符list,先取乘除運算符,再從操作數list中取該索引兩邊的操作數(對應運算符左右的操作數),計算後将運算結果放回原索引處。同樣地,再周遊list完成加減運算,直至list為空。

public class Calculate {
    public static String calculate(String exp){
        List<Character> oper = Operator(exp);
        List<String> fig = Figure(exp);
        for (int i = 0; i < oper.size(); i++) { //周遊運算符容器完成乘除運算
            Character op = oper.get(i);         //取得運算符
            if (op == '*' || op == '÷') {
                oper.remove(op);
                String l = fig.remove(i);
                String r = fig.remove(i);       //取得運算符左右兩側的操作數
                int lele, lden, rele, rden;     //分别定義兩個操作數的分子分母
                List<Integer> fra = ToFrac.tofrac(l, r);
                lele = fra.remove(0);
                lden = fra.remove(0);
                rele = fra.remove(0);
                rden = fra.remove(0);
                if (op == '*') {                      //乘法運算
                    fig.add(i, (lele * rele) + "/"    //将運算後的數添加在i位置
                            + (lden * rden));
                } else {
                    fig.add(i, (lele * rden) + "/"    //除法運算
                            + (lden * rele));
                }
                i--;}}
        while (!oper.isEmpty()) {                 //完成加減運算,為空時停止
            String result = null;
            Character op = oper.remove(0);
            String l = fig.remove(0);
            String r = fig.remove(0);
            int lele, lden, rele, rden;
            List<Integer> fra = ToFrac.tofrac(l, r);
            lele = fra.remove(0);
            lden = fra.remove(0);
            rele = fra.remove(0);
            rden = fra.remove(0);
            if (op == '+') {                                    //加法運算
                result = ((lele*rden) + (lden * rele))
                        + "/" + (lden * rden);
                fig.add(0, result);
            }
            if (op == '-') {                                    //減法運算
                result = ((lele * rden) - (lden* rele))
                        + "/" + (lden*rden);
                fig.add(0, result);
            }}
        return fig.get(0);
    }

    /*------------提取運算符--------------*/
    public static List<Character> Operator(String op){
        List<Character> list = new ArrayList<>();
        for (int i = 0; i < op.length(); i++) {
            if (op.charAt(i) == '+' || op.charAt(i) == '-'
                    || op.charAt(i) == '*' || op.charAt(i) == '÷') {
                list.add(op.charAt(i));}
        }
        return list;
    }

    /*-------------提取操作數-------------*/
    public static List<String> Figure(String fig){
        int n = 0;
        int count=0;
        int k=0;
        List<String> list = new ArrayList<>();
        for(int i=0;i<fig.length();i++){
            if (fig.charAt(i) == '+' || fig.charAt(i) == '-'
                    || fig.charAt(i) == '*' || fig.charAt(i) == '÷'
                    || fig.charAt(i) == '=') {
                k=i;           //運算符的index
                count++; }
        }
        if(count==1){
            list.add(fig.substring(0,k));
            list.add(fig.substring(k+1,fig.length()));}
        else {
            for (int i = 0; i < fig.length(); i++) {
                if (fig.charAt(i) == '+' || fig.charAt(i) == '-'
                        || fig.charAt(i) == '*' || fig.charAt(i) == '÷'
                        || fig.charAt(i) == '=') {
                    list.add(fig.substring(n, i));
                    n = i + 1;}}}
        return list;
    }}
           

NewCalculate類

查找表達式的括号,若無括号,則直接調用calculate();否則擷取括号内的子串,調用calculate(),用計算結果替換原子串,再遞歸調用newcalculate()。

public class NewCalculate {
    public static String newcalculate(String exp) {
        int lpar = exp.indexOf('(');       //在表達式中查找左括号
        if (lpar == -1) {
            return Calculate.calculate(exp);   //若沒有左括号則直接計算
        } else {
            int rpar = exp.indexOf(')');   //若有,則擷取該左括号對應的第一個右括号
            String expression = exp.substring(lpar + 1, rpar);
            exp = exp + "=";
            String ans = Calculate.calculate(expression);   //計算括号内的表達式
            if (ans.indexOf("-") != -1) {
                ans = "#"
                        + ans.substring(1, ans.length());
            }
            exp = exp.substring(0, lpar) + ans
                    + exp.substring(rpar + 1, exp.length());           //用計算結果替換帶括号的子串
            return newcalculate(exp);              //傳回運算結果
        }
    }
}
           

分數化簡

public class Simplify {      //分數化簡
    public static String gcd(String exp){
        int p=exp.indexOf('/');
        int ele,den,r,m,n=0;
        String result=null;
        ele=Integer.parseInt(exp.substring(0,p));
        den=Integer.parseInt(exp.substring(p+1,exp.length()));
        if(ele>den) {
             m=ele;
             n=den;}
        else{
            m=den;
            n=ele;}
        r=m%n;
        while (r != 0) {   //輾轉相除法求最大公約數
                m = n;
                n = r;
                r=m%n;
        }
        if(den/n==1)
            result=String.valueOf(ele/n);
        else
            result = (ele / n) + "/" + (den / n);
        return result;}
}
           

測試運作

程式運作結果如下圖所示:

2017202110105-進階軟體工程2017第2次作業

當使用者輸入不合法時,需重新輸入:

2017202110105-進階軟體工程2017第2次作業

測試方法:單元測試

測試對象:CreateExp() NewCalculate() ToFrac() Operator() Figure()

測試結果:測試均通過。

2017202110105-進階軟體工程2017第2次作業

代碼覆寫率如下(由于部分基礎方法沒有測試,故method coverage不為100%):

2017202110105-進階軟體工程2017第2次作業

測試代碼:

CreateExp

public class CreateExpTest extends TestCase {
    public void testexp() throws Exception{
        CreateExp ce=new CreateExp();
        ScriptEngineManager mgr = new ScriptEngineManager();
        ScriptEngine engine = mgr.getEngineByName("JavaScript");
        for(int i=0;i<1000;i++) {
            String str = ce.exp((int) ((Math.random() * 100) % 4 + 3)).replace('÷', '/');
            Assert.assertNotNull(engine.eval(str).toString());
        }
    }
}
           

ToFrac

public void testToFrac() throws Exception{
           List<Integer> list1 = new ArrayList(Arrays.asList(2,1,3,1));
           List<Integer> list2 = new ArrayList(Arrays.asList(2,3,4,1));
           List<Integer> list3 = new ArrayList(Arrays.asList(5,1,5,6));
           List<Integer> list4 = new ArrayList(Arrays.asList(1,2,2,3));
           Assert.assertEquals(list1,ToFrac.tofrac("2","3"));
           Assert.assertEquals(list2,ToFrac.tofrac("2/3","4"));
           Assert.assertEquals(list3,ToFrac.tofrac("5","5/6"));
           Assert.assertEquals(list4,ToFrac.tofrac("1/2","2/3"));
      }
           

Operator

public void testOperater() throws Exception{
           String str1="1+2÷3=";
           String str2="3+4*2+2/3=";
           String str3="3-3/4-1*2÷3=";
           String str4="2+1/4+1/3*3-3/4÷1/2=";
           String str5="3*5=";
           Assert.assertEquals(new ArrayList(Arrays.asList('+', '÷')),Calculate.Operator(str1));
           Assert.assertEquals(new ArrayList(Arrays.asList('+', '*','+')),Calculate.Operator(str2));
           Assert.assertEquals(new ArrayList(Arrays.asList('-', '-','*','÷')),Calculate.Operator(str3));
           Assert.assertEquals(new ArrayList(Arrays.asList('+', '+','*','-','÷')),Calculate.Operator(str4));
           Assert.assertEquals(new ArrayList(Arrays.asList('*')),Calculate.Operator(str5));
       }
           

Figure

public void testFigure() throws Exception{
           String str1="1+2÷3=";
           String str2="3+4*2+2/3=";
           String str3="3-3/4-1*2÷3=";
           String str4="2+1/4+1/3*3-3/4÷1/2=";
           String str5="3*5=";
           Assert.assertEquals(new ArrayList(Arrays.asList("1","2","3")),Calculate.Figure(str1));
           Assert.assertEquals(new ArrayList(Arrays.asList("3","4","2","2/3")),Calculate.Figure(str2));
           Assert.assertEquals(new ArrayList(Arrays.asList("3","3/4","1","2","3")),Calculate.Figure(str3));
           Assert.assertEquals(new ArrayList(Arrays.asList("2","1/4","1/3","3","3/4","1/2")),Calculate.Figure(str4));
           Assert.assertEquals(new ArrayList(Arrays.asList("3","5")),Calculate.Figure(str5));
       }
           

NewCalculate

public void testnewcalculate() throws Exception{
           String str1="(1+2)÷3=";
           String str2="3+4*2+2/3=";
           String str3="3-(3/4-1)*2÷3=";
           String str4="2+(1/4+1/3)*3-3/4÷1/2=";
           String str5="3*5=";
           Assert.assertEquals("1", Simplify.gcd(NewCalculate.newcalculate(str1)));
           Assert.assertEquals("35/3", Simplify.gcd(NewCalculate.newcalculate(str2)));
           Assert.assertEquals("19/6", Simplify.gcd(NewCalculate.newcalculate(str3)));
           Assert.assertEquals("9/4",Simplify.gcd(NewCalculate.newcalculate(str4)));
           Assert.assertEquals("15", Simplify.gcd(NewCalculate.newcalculate(str5)));
       }
           

項目小結

此次個人項目我花了較多時間完成,雖然算法原理不難,但真正實作起來總是會出這樣那樣的錯誤,使得程式設計進度變慢。大學階段學習資料結構時也沒有多加練習,隻是看着懂了就覺得掌握了,導緻動手能力弱。我花了大部分時間在生成表達式以及表達式計算上,期間也參考了其他人的解題方法,當我以為大功告成時,又發現了一些空指針異常以及數組越界異常,然後又調試好久才找出錯誤,究其原因是我對問題可能出現的情況沒有考慮完全,而這些小錯誤在我們開發時會被經常忽略。

通過這個項目,我也學習到了基本單元測試方法,使用了Junit。《建構之法》中說道:

軟體的很多錯誤都來源于程式員對子產品功能的誤解、疏忽或不了解子產品的變化,單元測試能使得子產品品質能得到穩定、量化的保證。

是以單元測試及其他測試技術的學習非常有意義。但由于時間限制,我沒能深入學習,在測試用例設計、覆寫率等方面都有待改進。總體來說本次項目還有很多需要改進和優化的地方,例如對程式進行效能分析、擴充程式功能等。

繼續閱讀