2017202110105-高级软件工程2017第2次作业

GitHub地址

https://github.com/setezzy/MyCalculator

PSP

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10 10
·Estimate · 预估时间 10 10
Development 开发 570 930
·Analysis · 需求分析(包括学习新技术) 30 40
·Design Spec · 生成设计文档 20 30
·Design Review · 设计复审 20 30
·Coding Standard · 代码规范 30 30
·Design · 具体设计 60 100
·Coding · 具体编码 240 480
·Code Review · 代码复审 30 40
·Test · 测试(自我测试,修改代码,提交修改) 120 180
Reporting 报告 150 150
·Test Report · 测试报告 120 120
·Size Measurement · 计算工作量 20 20
·Postmortem&Process Improvement Plan · 事后总结并提出过程改进计划 30 30
合计 730 1090

需求分析

目标

本软件的主要目标为:四则混合运算题目生成及计算

用户特点

本软件最终用户为小学生(或小学教师),会基本的四则混合运算,不会负数计算。故我们应基于上述用户特点进行软件开发。

假定和约束

开发方法:面向对象的开发技术
开发语言:Java
开发期限:1周

需求规定

由于软件规模较小,在此只考虑功能需求。以小学生为主要服务对象,对软件的功能初步设定为:
a.表达式生成功能。分为操作数随机生成,运算符随机生成以及最终表达式生成。操作数包括100以内正整数和真分数;运算符包括( +, -, *, ÷);最终表达式需要包含带括号的子表达式。其中操作数及运算符序列个数也随机生成。
b. 表达式计算功能。要求计算带括号的混合运算的结果,若最后结果为分数,则需要对分数进行化简。
c. 用户交互功能。首先判断用户输入是否非法,例如输入字母时要求用户重新输入;再对用户答案进行正确性判断:若正确,记得分,若错误,显示正确答案。最后显示用户的最终得分。
d. 用命令行参数n控制题目数量

功能图

运行环境规定

设备:PC
支持:PC支持Windows, Linux等操作系统
控制:本软件主要通过cmd运行,输入命令即可控制软件,无特殊要求。

详细设计

设计思路

a. 生成操作数数组:随机生成正整数或真分数,可由随机数决定。特别注意的是生成分数时,分子应小于分母。将操作数数目作为该方法变量,循环生成操作数,存入数组。
b. 生成运算符数组:随机生成四种运算符,通过switch()实现。当生成的运算符为乘或除时,考虑插入括号,且保证括号插入在低优先级运算符两侧:若乘/除号前一个是加/减号,可以插入括号,是否插入可由随机数决定。将操作数数目作为该方法变量,循环生成操作数,存入数组,当运算符数量达到最大值时,最后插入”=”。
c. 生成表达式:将a,b步骤生成的序列按序插入就得到一个符合逻辑的表达式。首先遍历操作数数组,当有括号时,判断是否为左括号,若是,则先插入括号,再依次插入数字和运算符和对应的右括号。
d. 表达式计算:在这里并未采用逆波兰表达式计算方法,而是参考了其他实现方法:先定义无括号的方法,具体实现为将表达式拆分分别放入list(分为操作数list和运算符list),接下来取运算符,取对应索引处左右的操作数,计算,并将计算结果放回当前索引位置。有括号的方法,具体实现为查找表达式中的括号,取出括号内的子表达式,调用无括号方法进行计算,用该计算结果替换该子串,得到新的表达式,再递归调用有括号方法。
e. 用户交互:用main()函数的String args[]参数接收用户定义的题目数量,循环调用c步骤的方法生成题目;接收用户输入的答案,若不合法则要求重新输入,将用户答案与d步骤得到的运算结果匹配,若相等,则显示“正确”,并计入得分,否则显示“错误”并显示正确答案。

设计实现

程序整体流程图

类图

核心代码说明

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

测试运行

程序运行结果如下图所示:

当用户输入不合法时,需重新输入:

测试方法:单元测试
测试对象:CreateExp() NewCalculate() ToFrac() Operator() Figure()
测试结果:测试均通过。

代码覆盖率如下(由于部分基础方法没有测试,故method coverage不为100%):

测试代码:
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。《构建之法》中说道:

软件的很多错误都来源于程序员对模块功能的误解、疏忽或不了解模块的变化,单元测试能使得模块质量能得到稳定、量化的保证。

所以单元测试及其他测试技术的学习非常有意义。但由于时间限制,我没能深入学习,在测试用例设计、覆盖率等方面都有待改进。总体来说本次项目还有很多需要改进和优化的地方,例如对程序进行效能分析、扩展程序功能等。

posted @ 2017-09-26 18:56  Roodo  阅读(337)  评论(2编辑  收藏  举报