软件工程作业3-结对编程项目 (龚超富 林嘉灏)

发布时间 2023-09-28 17:10:44作者: Gcf123
作业课程
Github项目地址 Github项目地址
姓名、学号 龚超富 3121005163
姓名、学号 林嘉灏 3121005175

题目需求

题目

实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。

自然数:0, 1, 2, …。

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

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

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

  • 四则运算题目:e = ,其中e为算术表达式。
需求
  1. 使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10

将生成10个题目。

  1. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10

将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。

  1. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
  2. 生成的题目中如果存在形如e1÷ e2的子表达式,那么*其结果应是真分数*
  3. *每道题目中出现的运算符个数不超过3个。*
  4. 程序一次运行生成的题目不能重复,*即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目*。例如,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。

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

  2. 答案1

  3. 答案2

特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。

  1. 程序应能支持一万道题目的生成。
  2. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt

统计结果输出到文件Grade.txt,格式如下:

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

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

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

PSP表格

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

效能分析

程序中消耗最大的函数是getTopicAnswer函数

设计实现过程

程序一共有八个类:Calculator、Core、IO、Expression、Query、Examine、Item Random、Main

方法关系组织

代码说明

随机构建中缀表达式。

  1. 初始化变量和数据结构:

    • 定义整数变量 i,用于遍历运算符数组。
    • 创建一个 ArrayList 类型的列表 inf,用于存储中缀表达式的各个元素(数字和运算符)。
  2. 遍历运算符数组和数字数组:

    • 使用 for 循环,遍历运算符数组 ope 中的每个元素。
    • 在循环中,首先将数字数组 num 中的对应元素添加到 inf 列表中。
    • 然后将当前运算符转换为字符串,并将其添加到 inf 列表中。
  3. 根据运算符数组的长度进行不同的处理:

    • 使用 switch 语句,根据运算符数组的长度(可能为3、2或其他),执行不同的分支。
    • 在某些分支中,根据特定条件,向 inf 列表的开头和结尾添加括号,以确保正确的运算顺序。
  4. 遍历 inf 列表,修正运算符周围的空格:

    • 使用 for 循环遍历 inf 列表中的每个元素。
    • 如果元素是运算符(+、-、×、÷),则在其前后添加空格,以确保表达式的格式正确。
  5. 构建最终的中缀表达式字符串:

    • 创建一个 StringBuilder 对象 infix,用于将 inf 列表中的元素连接起来,形成最终的中缀表达式字符串。
  6. 返回中缀表达式字符串:

    • infix 转换为字符串,并返回作为方法的结果。
public String infixExpression(char[] ope, String[] num) {
        int i = 0;
        ArrayList<String> inf = new ArrayList<>();
        for (; i < ope.length; i++) {
            inf.add(num[i]);
            inf.add(String.valueOf(ope[i]));
        }
        inf.add(num[i]);
        switch (ope.length) {
            case 3 -> {
                if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '+' || ope[1] == '-') && (ope[2] == '×' || ope[2] == '÷')) {
                    inf.add(0, "(");
                    inf.add(6, ")");
                }
                if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '×' || ope[1] == '÷')) {
                    inf.add(0, "(");
                    inf.add(4, ")");
                }
            }
            case 2 -> {
                if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '×' || ope[1] == '÷')) {
                    inf.add(0, "(");
                    inf.add(4, ")");
                }
            }
            default -> {
            }
        }
        for (i = 0; i < inf.size(); i++) {
            if (inf.get(i).equals("+") || inf.get(i).equals("-") || inf.get(i).equals("×") || inf.get(i).equals("÷"))
                inf.set(i, " " + inf.get(i) + " ");
        }
        StringBuilder infix = new StringBuilder(inf.get(0));
        for (i = 1; i < inf.size(); i++) {
            infix.append(inf.get(i));
        }
        return infix.toString();
    }

中缀表达式转化成后缀表达式

  1. 初始化数据结构:

    • 创建一个栈 stack 用于暂时存储运算符和左括号。
    • 创建一个列表 list 用于存储后缀表达式的各个元素。
    • 使用 split 方法将输入的中缀表达式字符串 string 按空格分割成字符串数组 splitString,以便逐个处理表达式的元素。
  2. 遍历中缀表达式的元素:

    • 使用 for-each 循环遍历 splitString 中的每个元素 str
    • 如果 str 是左括号,将其右边的内容加入到 list 中,并将左括号入栈。这是为了处理括号内的子表达式。
    • 如果 str 是右括号,将左括号到右括号之间的内容依次加入到 list 中,并将栈中的元素出栈,直到遇到左括号。这是为了处理括号内的子表达式。
    • 如果 str 是运算符(+、-、×、÷):
      • 如果栈为空,直接将当前运算符入栈。
      • 如果运算符是加法或减法,优先级最低,将栈中的运算符出栈并加入到 list 中,直到栈为空或栈顶是左括号。然后将当前运算符入栈。
      • 如果运算符是乘法或除法,其优先级较高。如果栈不为空且栈顶元素也是乘法或除法,将栈顶元素出栈并加入到 list 中,然后将当前运算符入栈。否则,直接将当前运算符入栈。
    • 如果 str 是数字或其他字符,直接将其加入到 list 中。
  3. 处理剩余栈内元素:

    • 循环将栈内的所有元素出栈并加入到 list 中,以确保所有元素都被处理。
  4. 将列表 list 中的元素转换为字符串数组 postfixString,并返回该数组作为方法的结果。

public String[] postfixExpression(String string) {
        // 符号栈
        Stack<String> stack = new Stack<>();
        // 后缀表达式
        List<String> list = new LinkedList<>();
        // 将中缀表达式按空格分开
        String[] splitString = string.split(" ");
        for (String str : splitString) {
            // 如果是左括号就入栈
            if (str.matches("\\(.*")) {
                list.add(str.split("\\(")[1]);
                stack.push("(");
            }
            // 如果是右括号就把栈顶元素依次加入到列表,直到读取到左括号,将其出栈。
            else if (str.matches(".*\\)")) {
                list.add(str.split("\\)")[0]);
                while (!stack.peek().equals("(")) {
                    list.add(stack.pop());
                }
                stack.pop();
            } else if (str.matches("[+\\-×÷]")) {
                // 栈为空将运算符入栈
                if (stack.isEmpty()) stack.push(str);
                    // 如果运算符是加减,优先级最低,将栈顶元素加入到列表,如果读取到左括号或栈为空将运算符入栈
                else if (str.matches("[+\\-]")) {
                    while (!stack.isEmpty() && !stack.peek().equals("(")) {
                        list.add(stack.pop());
                    }
                    stack.push(str);
                }
                // 如果运算符是乘除
                else {
                    // 如果栈不为空且栈顶元素是乘除,将其出栈加入到列表。
                    while (!stack.isEmpty() && stack.peek().matches("[×÷]")) {
                        list.add(stack.pop());
                    }
                    // 栈顶元素是加减或为空,将运算符入栈。
                    stack.push(str);
                }
            }
            // 其余符号都是表示数字,将其入栈。
            else {
                list.add(str);
            }
        }
        // 最后把栈内元素全部加入到列表
        while (!stack.isEmpty()) {
            list.add(stack.pop());
        }
        String[] postfixString = new String[list.size()];
        // 将列表元素转变为字符串数组
        for (int i = list.size() - 1; i >= 0; i--) {
            postfixString[i] = list.remove(i);
        }
        return postfixString;
    }

测试运行

  • 测试四则运算,设置不同输入,如果和理论的输出一致则通过。

    @Test
    public void calculate() {
        //加法
        assertThat(Calculate.calculate("1","3",1),equalTo("4"));
        assertThat(Calculate.calculate("1","1/5",1),equalTo("1'1/5"));
        //减法
        assertThat(Calculate.calculate("2","1",2),equalTo("1"));
        assertThat(Calculate.calculate("1/3","1",2),equalTo("-2/3"));
        assertThat(Calculate.calculate("3/5","2/5",2),equalTo("1/5"));
        //乘法
        assertThat(Calculate.calculate("1/2","2",3),equalTo("1"));
        assertThat(Calculate.calculate("4","2",3),equalTo("8"));
        assertThat(Calculate.calculate("3/4","5/6",3),equalTo("5/8"));
        assertThat(Calculate.calculate("1'1/2","1'1/2",3),equalTo("2'1/4"));
        //除法
        assertThat(Calculate.calculate("1/2","2",4),equalTo("1/4"));
        assertThat(Calculate.calculate("4","2",4),equalTo("2"));
        assertThat(Calculate.calculate("1'1/6","6",4),equalTo("7/36"));
        assertThat(Calculate.calculate("1'1/3","1'1/3",4),equalTo("1"));
    }
    
  • 测试分数转换成int型分子分母,分别测试带分数真分数自然数

    @Test
    public void conversion() {
        Calculate cal = new Calculate();
        assertThat(cal.conversion("1'1/3"),equalTo(new int[]{4,3}));
        assertThat(cal.conversion("1"),equalTo(new int[]{1,1}));
        assertThat(cal.conversion("5/4"),equalTo(new int[]{5,4}));
    }
    
  • 测试分子分母化简

    @Test
    public void reduction() {
        Calculate cal = new Calculate();
        assertThat(cal.reduction(9,7),equalTo("1'2/7"));
        assertThat(cal.reduction(3,4),equalTo("3/4"));
        assertThat(cal.reduction(9,2),equalTo("4'1/2"));
        assertThat(cal.reduction(20,10),equalTo("2"));
    }  
    
  • 测试两个表达式是否相同

    @Test
    public void sameExpression() {
        String a1="2 ÷ 1";
        String b1="1 - 2";
        Examine examine = new Examine();
        assertFalse(examine.sameExpression(a1, b1));
        String a2="1 + 2";
        String b2="2 + 1";
        assertTrue(examine.sameExpression(a2, b2));
        String a3="(1 ÷ 2) × 3";
        String b3="(2 ÷ 1) × 3";
        assertFalse(examine.sameExpression(a3, b3));
    }
    
  • 测试生成中缀表达式

    @Test
    public void infixExpression(){
        char[] ope={'+','-','×'};
        String[] num={"32","5","9","3/4"};
    
        Core core = new Core();
        assertThat(core.infixExpression(ope,num),equalTo("(32 + 5 - 9) × 3/4"));
        char[] ope1={'+','×'};
        String[] num1={"3","6","21","3/7"};
        assertThat(core.infixExpression(ope1,num1),equalTo("(3 + 6) × 21"));
        char[] ope2={'×'};
        String[] num2={"32","5","9","3/4"};
        assertThat(core.infixExpression(ope2,num2),equalTo("32 × 5"));
    }
    
  • 测试将中缀表达式转换成后缀表达式

    @Test
    public void postfixExpression(){
        String string="9 + (3 - 1) × 3 + 10 ÷ 2";
        Core core = new Core();
        String[] strings= core.postfixExpression(string);
        assertThat(strings[0],equalTo("9"));
        assertThat(strings[1],equalTo("3"));
        assertThat(strings[2],equalTo("1"));
        assertThat(strings[3],equalTo("-"));
        assertThat(strings[4],equalTo("3"));
        assertThat(strings[5],equalTo("×"));
        assertThat(strings[6],equalTo("+"));
        assertThat(strings[7],equalTo("10"));
        assertThat(strings[8],equalTo("2"));
        assertThat(strings[9],equalTo("÷"));
        assertThat(strings[10],equalTo("+"));
    
        String string1="a + b × c + (d × e + f) × g";
        String[] strings1= core.postfixExpression(string1);
        assertThat(strings1[0],equalTo("a"));
        assertThat(strings1[1],equalTo("b"));
        assertThat(strings1[2],equalTo("c"));
        assertThat(strings1[3],equalTo("×"));
        assertThat(strings1[4],equalTo("+"));
        assertThat(strings1[5],equalTo("d"));
        assertThat(strings1[6],equalTo("e"));
        assertThat(strings1[7],equalTo("×"));
        assertThat(strings1[8],equalTo("f"));
        assertThat(strings1[9],equalTo("+"));
        assertThat(strings1[10],equalTo("g"));
        assertThat(strings1[11],equalTo("×"));
        assertThat(strings1[12],equalTo("+"));
    }
    
  • 测试计算后缀表达式

    @Test
    public void generateAnswer() {
        String[] strings={"9","3","1","-","3","×","+","10","2","÷","+"};
        Core core = new Core();
        assertThat(core.generateAnswer(strings),equalTo("20"));
    }
    
  • 测试利用栈计算

    @Test
    public void identifyOperator() {
        Stack<String> stack=new Stack<>();
        Core core = new Core();
        stack.push("1");
        stack.push("3");
        core.identifyOperator(stack,"+");
        assertThat(stack.pop(),equalTo("4"));
        stack.push("1");
        stack.push("3");
        core.identifyOperator(stack,"-");
        assertThat(stack.pop(),equalTo("-2"));
        stack.push("1");
        stack.push("5");
        core.identifyOperator(stack,"×");
        assertThat(stack.pop(),equalTo("5"));
        stack.push("2");
        stack.push("2");
        core.identifyOperator(stack,"÷");
        assertThat(stack.pop(),equalTo("1"));
        core.identifyOperator(stack,"1'1/2");
        assertThat(stack.pop(),equalTo("1'1/2"));
    }
    
  • 测试主方法

    @Test
    public void main() {
        String[] strings={"-n","300","-r","120"};
        Main.main(strings);
        String[] strings1={"-e","exercise.txt","-a","answers.txt"};
        Main.main(strings1);
    }
    

项目小结

成功之处
  • 协作效率提高: 结对编程让我们可以即时解决问题,共同制定解决方案,并且迅速理解彼此的代码。这样做远比独自开发更高效。
  • 代码质量提高: 由于有两个人审查代码,我们能够尽早发现和修复潜在的错误,提高了代码质量。
  • 知识分享: 结对编程促进了我们之间的知识共享。
结对感受
  • 沟通交流是十分重要的一个部分,有效的沟通能促进工作的进行
  • 分工是一门学问,选择自己擅长的领域工作才是正确的选择

总的来说,结对编程是一个有益的实践,它加强了我们之间的合作,提高了项目质量。通过彼此分享经验,我们相信我们可以在未来的项目中取得更多的成功。