软工项目二:结对项目

一、GitHub地址:https://github.com/hhhh344/Arithmetic

项目合作者:3118005001 胡鹤腾 3118005003 黄济成
项目使用网址:http://120.78.187.151:8081/
(注:因为校验答案的算法有bug,如果上传了不合规格的文件就会炸,所以这里面只有生成表达式的功能)

二、题目叙述

2.1题目数字以及运算符要求:

  • 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
  • 自然数:0, 1, 2, …。
  • 运算符:+, −, ×, ÷。
  • 括号:(, )。
  • 等号:=。
  • 分隔符:空格(用于四则运算符和等号前后)。
  • 算术表达式:

    e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),

    其中e, e1和e2为表达式,n为自然数或真分数。

  • 四则运算题目:e = ,其中e为算术表达式。

2.2 生成题目具体操作过程及格式:

  • 使用 -n 参数控制生成题目的个数,例如: Myapp.exe -n 10 将生成10个题目。
  • 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如 :Myapp.exe -r 10 将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
  • 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
  • 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
  • 每道题目中出现的运算符个数不超过3个。
  • 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
  • 生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
  1. 四则运算题目1
  2. 四则运算题目2

   ……

 其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。

  • 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:

   答案1

     答案2

  • 真分数的运算如下例所示:1/6 + 1/8 = 7/24。
  • 程序应能支持一万道题目的生成。
  • 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:

  Myapp.exe -e .txt -a .txt 统计结果输出到文件Grade.txt,格式如下:

    Correct: 5 (1, 3, 5, 7, 9)

    Wrong: 5 (2, 4, 6, 8, 10)

  其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。

需求实现
需求描述 是否实现
控制生成题目的个数
控制题目中数值范围
计算过程不能产生负数
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数
每道题目中出现的运算符个数不超过3个
程序一次运行生成的题目不能重复
生成的题目存入执行程序的当前目录下的Exercises.txt文件
计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件
程序应能支持一万道题目的生成
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计

三、psp表

*PSP2.1* *Personal Software Process Stages* *预估耗时(分钟)* *实际耗时(分钟)*
Planning 计划 10 10
·Estimate · 估计这个任务需要多少时间 10 10
Development 开发 1440 2670
· Analysis · 需求分析 (包括学习新技术) 180 500
· Design Spec · 生成设计文档 60 60
·Design Review · 设计复审 (和同事审核设计文档) 60 60
·Coding Standard · 代码规范 (为目前的开发制定合适的规范) 60 60
· Design · 具体设计 180 360
· Coding · 具体编码 600 1000
· Code Review · 代码复审 100 200
· Test · 测试(自我测试,修改代码,提交修改) 200 360
Reporting 报告 120 120
· Test Report · 测试报告 60 60
·Size Measurement · 计算工作量 30 30
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
合计 1570 2730

四、效能分析

1 程序效能

2.消耗最多的函数

五、设计实现过程

六、代码说明

最主要的是先将表达式定义好,我们自定义的表达式如下

public class Expression {

    /**
     * parameterList 操作数列表
     */
    private List<Integer[]> parameterList;

    /**
     * operatorList 操作符列表
     */
    private List<String> operatorList;

    /**
     * result 运算结果
     */
    private Integer[] result;

    /**
     * pattern 表达式模型
     */
    private String pattern;

1、ExpressionDaoImpl类里面的几个方法

表达式定义好后,实现起来不难,这里就不贴代码了

将生成的表达式转化为字符串类型,方便将其存储再入栈或者写入文件

public String expressionToString(Expression exp) {
        String pattern = exp.getPattern();
    	//要被返回的字符串
        String returnString = "";

        List<Integer[]> parameterList = exp.getParameterList();
        Integer[] num;
        List<String> operatorList = exp.getOperatorList();
        String operator;

        int parameterIndex = 0;
        int operatorIndex = 0;
		//每一个表达式都存储着其模型,按照模型将其转化
        for(int i = 0; i < pattern.length(); i++) {
            char temp = pattern.charAt(i);
            switch (temp) {
                case '(':
                case ')':
                    returnString += temp + " ";
                    break;
                case 'n':
                    num = parameterList.get(parameterIndex++);
                    if(num[0] == 0) {
                        returnString += num[1] + " ";
                    }
                    else {
                        //如果带分数前面的整数为0,则只打印分数部分
                        if(num[1] != 0) {
                            returnString += num[1] + "'" + num[2] + "/" + num[3] + " ";
                        }
                        else {
                            returnString += num[2] + "/" + num[3] + " ";
                        }
                    }
                    break;
                case '#':
                    operator = operatorList.get(operatorIndex++);
                    if(operator.contains("/")) {
                        returnString += "÷ ";
                    }
                    else {
                        returnString += operator + " ";
                    }
                    break;
                default: 
            }
        }
        returnString += "=";
        return returnString;
    }

将字符串类型的表达式转化为自定义的表达式,用于读取文本文件里的表达式

public Expression stringToExpression(String expressionString) {
        Expression exp = new Expression();
        List<Integer[]> parameterList = new ArrayList<>();
        List<String> operateList = new ArrayList<>();
        String pattern = "";
        String[] str = expressionString.split("\\s");

        for (String item : str) {
            if(item.matches("^[()]$")) {
                pattern += item;
            }
            else if(item.matches("^[\\+\\-\\*÷]$")) {
                if("÷".equals(item)) {
                    operateList.add("/");
                }
                else {
                    operateList.add(item);
                }
                pattern += "#";
            }
            //整数
            else if(item.matches("^[0-9]+$")) {
                pattern += "n";
                Integer[] num = new Integer[2];
                num[0] = 0;
                num[1] = Integer.parseInt(item);
                parameterList.add(num);
            }
            //分数
            else if(item.matches("^([0-9]+')?[0-9]+\\/[0-9]+$")) {
                pattern += "n";
                Integer[] num = new Integer[4];
                num[0] = 1;
                if(item.contains("'")) {
                    num[1] = Integer.parseInt(item.substring(0, item.indexOf("'")));
                    num[2] = Integer.parseInt(item.substring(item.indexOf("'")+1, item.indexOf("/")));
                }
                else {
                    num[1] = 0;
                    num[2] = Integer.parseInt(item.substring(0, item.indexOf("/")));
                }
                num[3] = Integer.parseInt(item.substring(item.indexOf("/")+1));
                parameterList.add(num);
            }
            else if(item.matches("^=$") || !" ".equals(item)) {

            }
            else {
                throw new RuntimeException("将字符串表达式转化为Expression时出现了未知字符" + item);
            }
        }
        exp.setOperatorList(operateList);
        exp.setParameterList(parameterList);
        exp.setPattern(pattern);
        return exp;
    }

2、CalculateUtilsImpl类里面的几个方法

将表达式转化为后缀表达式

public Stack<String> getPostfixExpression(Expression expression) {
        Stack<String> S1 = new Stack<>();
        Stack<String> S2 = new Stack<>();

        List<Integer[]> parameterList = expression.getParameterList();
        Integer[] num;

        List<String> operatorList = expression.getOperatorList();
        String operator;

        int parameterIndex = 0;
        int operatorIndex = 0;

        //获取表达式的模式
        String pattern = expression.getPattern();
        S1.push("#");

        for(int i = 0; i < pattern.length(); i++) {
            char temp = pattern.charAt(i);
            switch (temp) {
                //若取出的字符是操作数,则分析出完整的运算数,该操作数直接送入S2栈
                case 'n':
                    num = parameterList.get(parameterIndex++);
                    if(num[0] == 0) {
                        S2.push(num[1].toString());
                    }
                    else {
                        S1.push("(");
                        S2.push(num[1].toString());
                        S1.push("+");
                        S2.push(num[2].toString());
                        S1.push("/");
                        S2.push(num[3].toString());
                        while(S1.peek() != "(") {
                            S2.push(S1.pop());
                        }
                        S1.pop();
                    }
                    break;
                case '#':
                    operator = operatorList.get(operatorIndex);
                    //1.如果S1为空,或栈顶为"(",则将该运算符进S1栈
                    if(S1.peek() == "#" || S1.peek() == "(") {
                        S1.push(operator);
                        operatorIndex++;
                    }
                    //2.如果该运算符优先级(不包括括号运算符)大于S1栈栈顶运算符优先级,则将该运算符进S1栈
                    else if(comparePriority(operator, S1.peek())) {
                        S1.push(operator);
                        operatorIndex++;
                    }
                    //3.否则,将S1栈的栈顶运算符弹出,送入S2栈中,跳回1
                    else {
                        S2.push(S1.pop());
                        i--;
                    }
                    break;
                //若取出的字符是“(”,则直接送入S1栈顶。
                case '(':
                    S1.push("(");
                    break;
                //若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
                case ')':
                    while(S1.peek() != "(") {
                        S2.push(S1.pop());
                    }
                    if(S1.peek() == "(") {
                        S1.pop();
                    }
                default:
            }
        }
        while(S1.peek() != "#") {
            S2.push(S1.pop());
        }
        return S2;
    }

传进一个表达式,计算表达式结果

public Integer[] getExpressionResult(Expression expression) {
		//将表达式转化为后缀表达式
        Stack<String> postfixExpression = getPostfixExpression(expression);
        ExpressionDaoImpl exp = new ExpressionDaoImpl();
        Stack<Integer[]> S3 = new Stack<>();
        Integer[] num1, num2, temp;

        for (String item : postfixExpression){
//            如果取出的元素是数字
            if(item.matches("[0-9]+")){
                S3.push(toInteger(item));
            }
//            如果取出的元素是操作符
            else if (item.matches("[\\+\\-\\*\\/]")){
//                栈顶元素应该在操作符后面
                num2 = S3.pop();
                num1 = S3.pop();
                temp = calculateTwoNumber(num1,num2,item);
//                如果两个数字不符合计算规则,除法出现被除数为零,减法出现负数
                if(temp[0]==4){
                    return temp;
                }
                S3.push(temp);
            }
        }
        if (S3.size()!=1){
            throw new RuntimeException("栈内元素剩余不等于1!" + S3.toString());
        }
        //将最终结果换为真分数
        Integer[] result = exp.getProperFraction(S3.peek());
        return result;
    }

3、FileUtilsImpl类里面的几个方法

读取文本文件时,将题号和表达式使用Map<>映射起来

public Map<Integer, String> getExpressionFileMap(File expressionFile) throws IOException {
        FileReader fr = new FileReader(expressionFile);
        BufferedReader br = new BufferedReader(fr);
        Map<Integer, String> expressionFileMap = new HashMap<>();
        String line = br.readLine();

        Integer number;
        Integer[] result;
        Expression expression;

        while(line != null && line != "\n") {
            number = Integer.parseInt(line.substring(0, line.indexOf(".")));
            expression = exp.stringToExpression(line.substring(line.indexOf(".")+1));
            result = cal.getExpressionResult(expression);
            expressionFileMap.put(number, cal.resultToString(result));
            line = br.readLine();
        }

        br.close();
        fr.close();
        return expressionFileMap;
    }

读取文本文件时,将题号和答案使用Map<>映射起来

public Map<Integer, String> getAnswerFileMap(File answerFile) throws IOException {
        FileReader fr = new FileReader(answerFile);
        BufferedReader br = new BufferedReader(fr);
        Map<Integer, String> answerFileMap = new HashMap<>();
        String line = br.readLine();

        Integer number;
        String[] answerString;

        while(line != null && line != "\n") {
            answerString = line.split(" ");
            number = Integer.parseInt(answerString[0].substring(0, answerString[0].indexOf(".")));
            if (answerString.length == 2) {
                answerFileMap.put(number, answerString[1]);
            }
            line = br.readLine();
        }

        br.close();
        fr.close();
        return answerFileMap;
    }

比较表达式文件里面的表达式和答案文件里面的答案是否相同,并将统计结果写入Grade

public boolean writeGradeInFile(File expressionFile, File answerFile, File gradeFile) throws IOException {
        FileWriter fw = new FileWriter(gradeFile);
        BufferedWriter bw = new BufferedWriter(fw);
        Map<Integer, String> expressionFileMap = getExpressionFileMap(expressionFile);
        Map<Integer, String> answerFileMap = getAnswerFileMap(answerFile);

        int correctCount = 0;
        int wrongCount = 0;

        String correctString = "Correct:(";
        String wrongString = "Wrong:(";

        for (Map.Entry<Integer, String> item : expressionFileMap.entrySet()) {
            Integer key = item.getKey();
            //比较两个答案字符串是否一致
            if(item.getValue().equals(answerFileMap.get(key))) {
                correctCount++;
                if(correctCount == 1) {
                    correctString += key;
                }
                else {
                    correctString += ", " + key;
                }
            }
            else {
                wrongCount++;
                if(wrongCount == 1) {
                    wrongString += key;
                }
                else {
                    wrongString += ", " + key;
                }
            }
        }
        correctString += ")";
        wrongString += ")";

        StringBuilder correctStringBuilder = new StringBuilder(correctString);
        StringBuilder wrongStringBuilder = new StringBuilder(wrongString);

        correctStringBuilder.insert(correctStringBuilder.indexOf("("), correctCount);
        wrongStringBuilder.insert(wrongStringBuilder.indexOf("("), wrongCount);

        bw.write(correctStringBuilder.toString());
        bw.newLine();
        bw.write(wrongStringBuilder.toString());

        bw.close();
        fw.close();

        if(wrongCount == 0) {
            return true;
        }
        return false;
    }

七、测试运行

1、如图所示,可以随机生成一万道题,数据范围为10(可用取值范围为2~10000),人工验证计算出的答案是正确

2、校验答案

和Grade文件里面的结果一致

八、项目小结

  1. ​ 按照结对项目的要求,整个项目的完成一直都是两个人合作实现的。
  2. ​ 第一天的时候并没用直接写代码,而是先分析了该项目需求,讨论了该创建哪种项目模式,和实体类的定义。
  3. ​ 第一次使用Java完成一个项目,一开始的时候很不熟练,所以前面的时候有不少时间在纠结创建哪些类,和哪些包,以及它们之间的关系。
  4. 主要遇到的问题有以下几个:
    1. 如何存储这些随机生成的数字,主要是分数
    2. 如何随机生成括号并且没有错误或者无意义
    3. 计算的时候因为分数要通分,导致的通分后分母可能过大而出现的溢出问题
    4. 判重问题(没有解决)
    5. 在git上遇到不少的问题,包括创建一个团队仓库,直接在IDEA上git,还有遇到的最大的麻烦就是合并分支出现的冲突问题,我们创建了一个develop分支,主要是在这个分支上提交,但我们也修改过master,所以在将其合并到master上的出现了冲突

5、个人总结:

​ 黄济成:收获,这一次项目是先规划了大部分内容,包括先写好了接口,写好注释,然后接下来才是开始写码实现,所以在中间实现的时候很流畅,只要去实现那些功能就可以了。然后就是写好一个功能后就会去写单元测试,虽然这个花了不少时间,但的确也发现了不少问题。实际开发时间远比规划时间多的主要原因是没有料到那些意外(比如git)会花如此多的时间。这种结对合作,让我在写代码的时候更集中注意力,遇到问题的时候更是能集中两个人的智慧解决,这次的项目十分感谢我的合作伙伴,让我学到了非常多的东西。
胡鹤腾: 整个项目开发过程中我们花了比较多的时间去学习、构思项目结构的构建,git以及合作的方法。一开始要进行不能面对面的合作交流是比较困难的,后来我们适应了用腾讯会议来交流思路、互相监督以及用git共同管理仓库,这还是足够便利的。这些合作技能也是我在这次项目中最大的收获,还有一些收获是编程能力的提高、学习能力的提高。遗憾的是代码写得不是很规范,第一次使用JAVA写完整的项目,我们后来意识到这点但也难以改动,但是下次会做得更高效、规范。

posted @ 2020-04-13 21:02  戮漠  阅读(231)  评论(0编辑  收藏  举报