结对项目

软件工程 https://edu.cnblogs.com/campus/gdgy/InformationSecurity1912-Softwareengineering
作业要求 https://edu.cnblogs.com/campus/gdgy/InformationSecurity1912-Softwareengineering/homework/12147
作业目标 实现一个自动生成小学四则运算题目的程序,并且能够对题目和学生答题的答案进行比较,判断学生的答题情况
项目成员1 3119005483 叶臻强
项目成员2 3119005464 黎梓洋

源码链接

已经发布在GitHub上:https://github.com/yzqctf/3119005483/tree/main/Arithmetic

PSP表格

PSP 各个阶段 自己预估的时间(分钟) 实际的记录(分钟)
计划: 明确需求和其他因素,估计以下的各个任务需要多少时间 20 20
开发 (包括下面 8 项子任务) (以下都填预估值) 650
需求分析 (包括学习新技术) 30 60
生成设计文档 15 15
设计复审 30 40
代码规范 60 40
具体设计 10 15
具体编码 420 300
代码复审 60 120
测试 30 60
报告 60 60
测试报告 30 30
计算工作量 (多少行代码,多少次签入,多少测试用例,其他工作量) 30 30
事后总结, 并提出改进计划 (包括写文档、博客的时间) 60 60
总共花费的时间 (分钟) 855 850

需求分析

  1. 运算符个数不超过3个;
  2. 题目不能重复;
  3. 生产题目文件存在当前目录下的Exercises.txt;
  4. 计算出所有题目的答案并存到当前目录下的Answers.txt中;
  5. 能够支持10000道题目的生成;
  6. 判断答案的对错并进行数量统计
  7. 运算结果不能是负数;
  8. 除法的输出结果是真分数
  9. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
  10. 使用 -n 参数控制生成题目的个数

通过上述需求不难分析出:程序需要接收四个参数-r, -n, Exercises path, Answers path, 分别对应表达式中数值的范围 ,Exercises.txt的路径和Answers.txt的路径,不能重复需要自己指定重复的标准;现规定如下:

  1. 结果是否已经出现过:如果没出现过,不会重复(这个条件已经能筛选出很多符合标准的答案)
  2. 如果结果已经出现过,只有完全相同才算重复,即可通过表达式的逆波兰表达式进行检查

具体实现

实现流程

  1. 生成表达式
    1. 整数
    2. 分数
    3. 带分数
    4. 带括号
  2. 计算
    1. 转化为逆波兰表达式
    2. 整数一套最简单的规则
    3. 分数:统一形式成 'a/b' 表示b分之a;注:'1'1/2':表示1又1/2,即3/2再计算
    4. 分数约分
      1. 找最大公因数
      2. 转化为带分数
    5. 返回符合格式的答案
  3. 判断题目是否重复
  4. 生成题目文件,答案文件
  5. 计算错题数,以及错题所在题号

key words:随机生成表达式,统一表达式,最大公因数约分,转化为合适的输出格式

具体代码实现

在本例中,使用三个类完成四则计算器的实现

其中GUI为图形化界面,所以主要实现还是由AlgoArithmetic与AlgoCalculate完成

生成表达式

//生成表达式,f表示是否生成带括号表达式
public static String finalFormula(boolean f){
        String formula = new String();
        Random random = new Random();
        int numberOfOperation = random.nextInt(OPERATORNUMBER)+1;
        if (numberOfOperation==1){
            formula += getRandomNumber();
            formula +=" " + getRandomOperator();
            formula +=" " + getRandomNumber();
            numberOfOperation--;
            return formula;
        }
        if (!f){
            //不需要括号
            formula += getRandomNumber();
            while (numberOfOperation!=0){
                formula +=" " + getRandomOperator();
                formula +=" " + getRandomNumber();
                numberOfOperation--;
            }
        }else {
                int first = random.nextInt(2);
//            System.out.println("括号位置:"+first);
                if (first==0){
                    formula += BRACKETS[0];
                    formula +=" "+ getRandomNumber();
                    formula +=" " + getRandomOperator();
                    formula +=" " + getRandomNumber();
                    formula +=" "+ BRACKETS[1];
                    formula +=" " + getRandomOperator();
                    formula +=" " + getRandomNumber();
                }else if (first==1){
                    formula += getRandomNumber();
                    formula +=" " + getRandomOperator();
                    formula +=" "+BRACKETS[0];
                    formula +=" "+ getRandomNumber();
                    formula +=" " + getRandomOperator();
                    formula +=" " + getRandomNumber();
                    formula +=" "+ BRACKETS[1];
                }
        }
        return formula;
    }
//生成随机数(整数或真分数),范围可控,根据输入的-r进行判断
    public static String getRandomNumber() {
        //不能生成负数,所以简单了
        String num = new String();
        Random random = new Random();
        if (random.nextInt(2)==0){
            num = String.valueOf(random.nextInt(RANGEVALUE)+1);
        }else {
            //分子
            int molecule = random.nextInt(RANGEVALUE)+1;
            //分母
            int denominator = random.nextInt(RANGEVALUE)+1+molecule;
            num = molecule+"/"+denominator;
        }
        return num;
    }

其中有全局变量,可以在GitHub上看源代码,都有着注释。

调用计算接口
//随机带括号的题目
int numOfBracketProblems = random.nextInt(NUMBER_PROBLEMS-1)+1;
        System.out.println("带括号的题目数: " + numOfBracketProblems);
        NUMBER_PROBLEMS -= numOfBracketProblems;
        while (numOfBracketProblems!=0){
            String formula = finalFormula(true);
//            System.out.println(formula);
            //是否重复
            boolean ok = end(formula);
            if (!ok)
                continue;
            //System.out.println(formula+" = ");
            numOfBracketProblems--;
        }
        while (NUMBER_PROBLEMS!=0){
            String formula = finalFormula(false);
//            System.out.println(formula);
            boolean ok = end(formula);
            if (!ok)
                continue;
            //System.out.println(formula+" = ");
            NUMBER_PROBLEMS--;
        }
//返回布尔值,判断是否重复
public static boolean end(String formula){
        String re = new AlgoCalculate(formula).result;
        if (re==null){
            re="0";
        }
        if (re.contains("-"))
            return false;
        //判断重复
        if (out.containsKey(re))
            return false;
        FORMULA.add(formula);
        out.put(re,formula);
        return true;
    }

计算AlgoCalculate

在AlgoCalculate类中具体实现了以下方法

下面放一些主要方法的源码

//将formula转化为逆波兰表达式,即后缀表达式
    public static List<String> initRPN(String formula){
        List<String> rpn = new ArrayList<>();
        //存放操作符的栈
        Stack<String> stack = new Stack();
        String operation = "(+-*÷)";
//        System.out.println(formula.length());
        String[] split = formula.split(" ");
        int length = split.length;
        for (int i=0;i<length;i++){
            String str = split[i];
            if (isNumber(str)){
                rpn.add(str);
            }else {
                if (str.equals("(")){
                    //直接入栈
                    stack.push(str);
                }else if (str.equals(")")){
                    //出栈,直到遇到'('或者栈空
                    while (!stack.empty()){
                        String temp = stack.pop();
                        if (!temp.equals("(")){
                            //如果不是左括号
                            rpn.add(temp);
                        }else
                            break;
                    }
                } else {
                    //如果此使栈为空了
                    if (stack.empty())
                        stack.push(str);
                    else {
                        //栈不空就需要进行运算发符比较
                        //compare
                        if (compare(stack.peek())>=compare(str)){
                            while (!stack.empty()&&compare(stack.peek())>=compare(str))
                                rpn.add(stack.pop());
                        }
                        stack.push(str);
                    }
                }
            }

        }
        while (!stack.empty())
            rpn.add(stack.pop());

        return rpn;
    }
//具体的运算实现 
//b 运算 a
    public static String calculate(String a, String b, String symbol){
        String result=null;
        //是否存在分数:
        boolean flag = false;
        //统一形式
        //1.带分数
        if (a.contains("'")){
            String[] str = a.split("'");
            String[] str1 = str[1].split("/");
            int numerator = Integer.parseInt(str[0])*Integer.parseInt(str1[1])+Integer.parseInt(str1[0]);
            a = numerator+"/"+str1[1];
        }
        if (b.contains("'")){
            String[] str = b.split("'");
            String[] str1 =str[1].split("/");
            int numerator = Integer.parseInt(str[0])*Integer.parseInt(str1[1])+Integer.parseInt(str1[0]);
            b = numerator+"/"+str1[1];
        }
        if (a.contains("/")||b.contains("/")) flag=true;
        if (flag){
            boolean w = false;
            if (a.contains("/")&&b.contains("/")) w=true;
            //只有一个分数,转换格式;
            if (!w){
                if (a.contains("/")){
                    String[] x = a.split("/");
                    int numerator = Integer.parseInt(b)*Integer.parseInt(x[1]);
                    b = numerator+"/"+x[1];
                }else if (b.contains("/")){
                    String[] x = b.split("/");
                    int numerator = Integer.parseInt(a)*Integer.parseInt(x[1]);
                    a = numerator+"/"+x[1];
                }
            }
        }
        if (flag){
            int ans=0;
            String[] x = a.split("/");
            String[] x1 = b.split("/");
            if (symbol.equals("*")){
                    //分母
                    int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
                    //分子
                    int numerator = Integer.parseInt(x[0])*Integer.parseInt(x1[0]);
                    if (denominator==0||numerator==0)
                        result="0";
                    else
                        result = reduction(denominator,numerator);
                }
            else if (symbol.equals("÷")){
                //两分数相÷
                int denominator = Integer.parseInt(x1[1])*Integer.parseInt(x[0]);
                int numerator = Integer.parseInt(x1[0])*Integer.parseInt(x[1]);
                if (denominator!=0&&numerator!=0)
                    result = reduction(denominator,numerator);
                else
                    result = "0";
            }else if (symbol.equals("+")){
                int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
                int numerator = Integer.parseInt(x[0])*Integer.parseInt(x1[1]) + Integer.parseInt(x1[0])*Integer.parseInt(x[1]);
                if (denominator==0||numerator==0)
                    result="0";
                else
                    result = reduction(denominator,numerator);
            }else if (symbol.equals("-")){
                int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
                int numerator = Integer.parseInt(x1[0])*Integer.parseInt(x[1]) - Integer.parseInt(x1[1])*Integer.parseInt(x[0]);
                if (numerator==0||denominator==0){
                    result="0";
                }
                else if (numerator<0)
                    result = numerator+"/"+denominator;
                else
                    result = reduction(denominator,numerator);
            }
        }else {
            int tempNumber1 = Integer.parseInt(a);
            int tempNumber2 = Integer.parseInt(b);
            int ans=0;
            if (symbol.equals("+"))
                ans = tempNumber2+tempNumber1;
            else if (symbol.equals("-"))
                ans = tempNumber2-tempNumber1;
            else if (symbol.equals("*"))
                ans = tempNumber2*tempNumber1;
            else if (symbol.equals("÷")){
                if (tempNumber1!=0){
                    if (tempNumber2%tempNumber1==0)
                        ans = tempNumber2/tempNumber1;
                    else {
                        return result = reduction(tempNumber1,tempNumber2);
                    }
                }else {
                    ans=0;
                }
            }
            result = String.valueOf(ans);
        }
        return result;
    }

运行结果展示

  1. 题目文件

  1. 答案文件

  1. 运行界面

  1. 检查作业

通过对比发现,检查作业这一功能得以实现。

测试类

  1. 整体程序测试(不含图形化界面)

  2. 表达式计算

public void calculate(){
        String re;
        //加法
        String f1 = "4 + 3 * 3/4";
        re = new AlgoCalculate(f1).result;
        System.out.println("加法:"+re);

        //减法出现负数
        String f2 = "3 - 4";
        boolean ok = AlgoArithmetic.end(f2);
        if (!ok) System.out.println("结果为负数,请重新输入");

        //乘法
        String f3 = "6 * ( 1/3 * 6 )";
        re = new AlgoCalculate(f3).result;
        System.out.println("乘法:"+re);

        //除法
        String f4 = "8/16 ÷ 6/10 ÷ 9/14 ÷ 9";
        re = new AlgoCalculate(f4).result;
        System.out.println("除法:"+re);

        //除法分母为0,注:分母为0的条件下自定义结果为0,虽然可能是无穷大
        String f5 = "10 ÷ ( 5 - 5 )";
        re = new AlgoCalculate(f5).result;
        System.out.println("分母为0:"+re);

    }

  1. 出现未完成全部题目的情况
    //学生未完成作业
    @Test
    public void falseWork() throws IOException {
        AlgoArithmetic.check("D:\\学习资料\\softwareProject\\Arithmetic\\Answers.txt","D:\\学习资料\\softwareProject\\Arithmetic\\Exercises.txt");
    }

这里测试是完全没答题的情况下:

程序性能展示

  1. 代码覆盖率

  1. 时间、内存使用情况

代码审查


码风问题在这里就不再重复修改了,会不断学习的!!
其中的三个erro prone为Test测试单元中没有写assert()或fail()


从结果来看,代码的问题较上次提交个人项目减少了4%,复杂度增加了4%,代码重复度从上次的17%到现在的20%,20%总体来说符合自主完成作业没有抄袭的要求。

总结

成员:叶臻强

  1. 一定要留够摸鱼时间!!!国庆写完了表达式生成,剩下的计算规则代码完成硬是拖到了最后一刻,虽然期间也有别的事情要处理,但是完成一个项目就按照最坏的情况去安排时间就是对的;
  2. 原本以为逆波兰表达式(后缀表达式)已经学过,上手驾轻就熟,但是不要高估自己的动手实践能力,总是会有奇奇怪怪的问题的,所以要尽早开始学习文档;
  3. 命名一定要做到“见字如面”,要不然写方法的时候会搞不清变量之间的关系,深刻例子:哪一个是减数,哪一个是被减数(同理除法);
  4. 同样的代码一定要提取公因式,减少整体的冗余,同样,处理问题的时候尽量把可能输入的问题进行统一格式,处理一种格式总比不断的分情况讨论简单;
  5. 结对项目最好的一点是各有所长,我做好我的部分之后,剩下的就不需要考虑了,除了解释怎么调用之外。

成员:黎梓洋

  1. 在本次项目中,我主要负责图形化界面的实现以及代码接口的测试;
  2. 修改一些bug,增强代码的健壮性;
  3. 在本次项目中知道了项目的分工以及合作的重要性;
posted @ 2021-10-25 21:36  火烛  阅读(103)  评论(0编辑  收藏  举报