结对项目:Java实现自动生成小学四则运算题目

发布时间 2023-09-28 22:13:58作者: AFlicker

结队项目

软件工程 所在班级
队伍成员 谢昊天(3121004672) 林育鑫(3121004660)
作业要求 [要求](([个人项目 - 作业 - 计科21级12班 - 班级博客 - 博客园 (cnblogs.com)](结对项目 - 作业 - 计科21级12班 - 班级博客 - 博客园 (cnblogs.com))))
作业目标 实现一个自动生成小学四则运算题目的命令行程序,学会团队协作配合开发
Github GitHub仓库地址

效能分析

  1. 在初始化正确与错误的数组时,由于测试的List没有初始化容量,在测试的数据有100时,由于容量不够而产生了后续的扩容机制,于是在每次新建时指定了List的容量,减少了List容器在扩容上的损耗。
  2. 在IO操作时 发现传统的IO操作方法比较费时,使用Java的NIO库(本质是IO的多路复用)优化对文件的IO操作

image

设计实现过程

在代码中 有以下结构

其中 有service包,utils包与Main函数

service层
Calculate类
  • 主要包含将中缀表达式转化成后缀表达式的 toSuffixExpression()方法
  • 以及对操作符的判断isDigital(char c)以及对是否是数字的判断isDigital(char c)方法
  • 对运算符优先级的判断( 比如括号优先级>乘除>加减) 的int priority(char c)方法
  • 考虑到对分数的计算,用Fraction calculate(Queue queue) 方法直接把数字转化为fraction对象,再压入fraction栈
分数类Fraction
  • 主要包含两个变量 numerator以及denominator分别存放分子与分母
  • 同时包含了对分数的加减乘除操作以及判断操作是否合法,比如除数不能为0
  • 对分数进行约分化简的方法 transferFraction(Fraction fraction)
实现需求9的Judge类
  • 作用为 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
  • 主要有JudgeTrueOrWrong(String questionTxt, String answerTxt)方法获取用户题目和答案文本
  • 而addAnswerToList(List list,String path)方法则将文件中的字符串将进list中 便于后续获取
utils层
FileUtils类
  • 包含将目标文件加载为一个字符串getFileText(String path)
  • 将字符串保存到目标文件中 writeToFile(String path,String content)
QuestionUtils类
  • 根据限制生成随机题目generateExp(Integer limit)
  • 生成所需的题目数量并检验数据合法性 legalExp(Integer number, Integer limit)
StringUtils
  • isPositiveInteger(String str)通过正则表达式判断用户输入的数是否为正整数
Main函数
  • 作为程序的主要入口
  • 判断参数是否合法 并调用相关的处理方法
Test层


有上面源代码各个方法的测试函数

代码说明

校验操作符优先级
private int priority(char c) {
        switch (c) {
            case '+':
            case '-':
                return 1;
            case '*':
            case '/':
                return 2;
            case '(':
                return 0;//左括号的优先级最低
            default:
                throw new RuntimeException("Illegal operator:" + c);
        }
    }
通过将问题中缀表达式转化为后缀表达式,再通过栈来计算表达式的值 核心代码为
public Queue<String> toSuffixExpression(String s) {//生成后缀表达式
        Stack<Character> stack = new Stack<>();
        Queue<String> queue = new LinkedList<>();

        int index = 0;//字符串标签
        while (index < s.length()) {//str.length()表示元素的个数

            char c = s.charAt(index);
            // 如果是数字,就入队列
            if (isDigital(c)) {
                // 入队的时候要判断后面是否还有剩余的数字,要把整个数字入队列,而不是一个数字字符
                // 在多位数的时候有用
                int p = index;
                while (p < s.length() && isDigital(s.charAt(p))) {
                    p++;
                }
                queue.add(s.substring(index, p));//截取字符串,从指定位置(start)开始截到指定的位置(end)
                index = p;
                continue;
                // 如果是左括号,就入栈
            } else if (c == '(') {
                stack.push(c);
                // 如果是右括号,就弹出栈中的元素,直到遇到左括号为止。左右括号均不入队列
            } else if (c == ')') {
                while ('(' != stack.peek()) {
                    queue.add(stack.pop() + "");
                }
                // 弹出左括号
                stack.pop();
                // 如果是运算符,分下面的情况讨论
            } else if (isOperator(c)) {
                // 如果符号栈为空,就直接压入栈
                if (stack.isEmpty()) {
                    stack.push(c);
                    // 如果符号栈的栈顶是左括号,则压入栈中
                } else if ('(' == stack.peek()) {
                    stack.push(c);
                    // 如果当前元素的优先级比符号栈的栈顶元素优先级高,则压入栈中
                } else if (priority(c) > priority(stack.peek())) {//debug将大于出栈
                    stack.push(c);
                    // 如果此时遍历的运算符的优先级小于等于此时符号栈栈顶的运算符的优先级,
                    // 则将符号栈的栈顶元素弹出并且放到队列中,并且将正在遍历的符号压入符号栈
                } else if (priority(c) <= priority(stack.peek())) {
                    /*queue.add(stack.pop() + "");
                    stack.push(c);*/
                    queue.add(stack.pop() + "");
                   while(  !stack.isEmpty() && priority(c) == priority(stack.peek()) ) {
                        queue.add(stack.pop() + "");
                    }
                    stack.push(c);
                }
            }
            index++;
        }

        // 遍历完后,将栈中元素全部弹出到队列中
        while (!stack.isEmpty()) {
            queue.add(stack.pop() + "");
        }


        return queue;
    }
生成随机题目(包含题目)
 //生成题目 带括号
    public static String generateExp(Integer limit) throws StringIndexOutOfBoundsException{
        Random rd = new Random();

        // 生成四个随机数
        int e1 = rd.nextInt(limit);
        int e2 = rd.nextInt(limit);
        int e3 = rd.nextInt(limit);
        int e4 = rd.nextInt(limit);

        // 保存生成的题目
        StringBuilder str = new StringBuilder();

        int[] num = { e1, e2, e3, e4 }; // 数字
        char[] opt = { '+', '-', '*', '/' }; // 运算符
        int[] no = { 1, 2, 3 }; // 控制选择的运算符(0-3)
        int numOperators = no[rd.nextInt(3)]; // 控制运算符的数目

        for (int j = 0; j < numOperators; j++) {
            str.append(num[rd.nextInt(4)]).append(" "); // 添加数字和空格
            str.append(opt[rd.nextInt(4)]).append(" "); // 添加运算符和空格
        }
        str.append(num[rd.nextInt(4)]); // 添加最后一个数字
        int index = 0; // 记录数组的下标
        int len = 0;
        String left = "";
        String right = "";
        String remainder = "";

        // 添加括号
        int[] indexs = { 0, 4, 8 }; // 从indexs开始加左括号
        int[] lens = { 5, 9, 13 }; // 括号的宽度,即从从indexs+len开始加右括号
        index = indexs[rd.nextInt(3)];
        len = lens[rd.nextInt(3)];

        //随机添加括号
        if (index + len < str.length() - 1 || (index + len == str.length() + 1 && index != 0)) {
            left = str.substring(0, index) + "(";
            right = str.substring(index, index + len) + ")";
            remainder = str.substring(index + len);
        }
        //不能生成括号
        if (left==""){
            return str.toString();
        }
        return left+right+remainder;

    }
按数量生成题目 并校验合法性 以及最终将生成的题目和答案保存在文件夹中
public static void legalExp(Integer number, Integer limit) throws IOException {


        HashMap<String, Integer> answers = new HashMap<String, Integer>();
        String questionFile="";
        String answerFile="";

        for (int j = 1; j <= number; j++) {
            String question = QuestionUtils.generateExp(limit) + " = ";//获得原始表达式
            Calculate cal = new Calculate();
            Fraction f = cal.outcome(question);

            //返回100000 说明不能计算
            if (f.getNumerator() == 100000) {

                j--;
                continue;
            }

            String result = f.transferFraction(f);//最终结果,已经化简

            if(answers.containsKey(Calculate.temp)){
                j--;
                continue;
            }else{
                answers.put(Calculate.temp, null);
                questionFile += j+"."+"    "+question+"\n";
                answerFile += j+"."+"    "+question+result+"\n";
            }
        }

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a341f0293cf4082b11e39c2da7dcdf8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1835&h=441&s=48654&e=png&b=fcfcfc)
        System.out.println("表达式生成完毕,进行存入文件");

        FileUtils.writeToFile("D:\\IdeaTestProject\\Four-Arithmetic-Operations\\src\\main\\resources\\question.txt",questionFile);//整个字符串
        FileUtils.writeToFile("D:\\IdeaTestProject\\Four-Arithmetic-Operations\\src\\main\\resources\\answer.txt",answerFile);//整个字符串

    }

测试运行

  1. test包中所有方法测试通过

  2. 测试分数是否能正常表示以及运算

  3. 根据答案的txt文档获取答案

  4. 根据题目文本获取题目 并计算结果

  5. 测试题目以及对应答案的生成

  6. 对给定的题目文件和答案文件,判定答案中的对错并进行数量统计

整体项目打包测试
  1. 当不输入生成题目的个数
    题目文件

    控制台

  2. 指定生成10000个题目
    题目文件

    答案文件

  3. 当不指定操作数的范围或者范围不为正整数时 报错
    控制台输出

  4. 根据对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
    将生成的答案文件修改前5个为1 来测试答案对错 即故意将前5个答案变错

    控制台输出

将结果保存的Grade文件

PSP阶段

PSP阶段 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 50 45
Estimate 估计这个任务需要多少时间 360 340
Development 开发 100 95
Analysis 需求分析 (包括学习新技术) 20 10
Design Spec 生成设计文档 30 30
Design Review 设计复审 20 20
Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 10
Design 具体设计 30 20
Coding 具体编码 150 180
Code Review 代码复审 30 20
Test 测试(自我测试,修改代码,提交修改) 150 160
Reporting 报告 100 110
CodingTest Repor 测试报告 60 40
Size Measurement 计算工作量 30 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 30 20
合计 1170 1120

项目小结

林育鑫:这个结对项目的经验对我来说是非常有意义的。首先,我很高兴能够与我的搭档一起合作,我们都有很好的沟通和合作能力。我们的目标是编写一个能够自动生成小学四则运算题目的命令行程序。
对于我的搭档,我觉得他在项目中的闪光点是他的设计和测试能力。他设计了一个简洁而直观的用户界面,并且通过测试和调试确保程序的稳定性和正确性。

谢昊天:对于我的搭档,我非常感激他在项目中的付出和贡献。他的编程能力和逻辑思维非常出色,解决问题的能力也非常强。他在项目中的闪光点是他能够快速理解和实现复杂的算法,负责了程序的核心功能的实现,并且能够与我有效地沟通和合作。

总结:在项目的开始阶段,我们共同制定了项目的目标和需求,并且明确了每个人的角色和任务分工。我们决定使用Java语言来编写程序,并且使用面向对象的设计方法。
在编写代码的过程中,我们充分发挥了各自的优势。

在工作过程中,我们经常进行代码审查和交流。我们互相学习和借鉴对方的编程技巧和思维方式。我们的沟通十分顺畅,遇到问题时能够及时解决。然而,在项目的过程中也存在一些挑战和教训。我们在初期对项目的需求和功能设计上花费了较多的时间,导致进度比较缓慢。同时,在编码过程中,我们也遇到了一些难以解决的问题,需要花费更多的时间和精力来解决。

但是,通过这个项目,我学到了很多。我们更加熟悉了Java编程语言,也提高了自己的编码能力。我们也学会了如何与他人合作,并且在合作中提升了自己的团队合作能力。
总的来说,这个结对项目是一次非常有意义的经历。通过与搭档的合作,我们都学到了很多,并且提升了自己的编程能力和团队合作能力。我们相信这个项目的成功离不开我们的共同努力和合作精神。