双人合作项目-四则运算生成器

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023/homework/13326
这个作业的目标 熟悉体会双人合作构建项目的流程,深入理解项目实现过程中的分工与交流的重要性所在
项目成员 夏钦涛3123004328/秦嘉胜3123004238

一、GitHub仓库地址(请点击对应成员姓名⬆️)

二、PSP表

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

三、题目需求

题目:实现一个自动生成小学四则运算题目的命令行程序

实现一个生成小学四则运算题目的程序,支持命令行或图像界面,功能如下:

  1. 生成题目:

    • 使用 -n 参数控制题目数量,如 -n 10 生成10道题。
    • 使用 -r 参数控制数值范围,如 -r 10 生成10以内的题目,该参数必填。
    • 题目要求:
      • 计算过程不产生负数。
      • 除法结果为真分数。
      • 每题运算符不超过3个。
      • 题目不重复。
    • 题目和答案分别保存到当前目录的 Exercises.txtAnswers.txt 文件中,真分数格式为 3/52'3/8
  2. 批改题目:

    • 使用 -e-a 参数分别指定题目文件和答案文件,如 -e Exercises.txt -a Answers.txt
    • 统计结果输出到 Grade.txt 文件,格式为:
      Correct: 5 (1, 3, 5, 7, 9)
      Wrong: 5 (2, 4, 6, 8, 10)
      
    • 假设输入题目按顺序编号且符合规范。

程序需支持生成最多10000道题目。

四、代码实现

1.设计实现过程

以下是每个类的功能描述:

Fraction.java

这个类表示一个分数,可以是假分数或整数。

  • 构造函数:接受分子和分母,创建一个新的 Fraction 对象。
  • 简化函数:将分数化简为最简形式。
  • 加减乘除运算:提供加(add)、减(subtract)、乘(multiply)和除(divide)方法来计算两个分数的和、差、积和商。
  • 字符串表示toProperFractionString 方法返回分数的字符串表示形式,符合题目要求的格式(例如:2’3/8)。

ArithmeticCalculator.java

这个类用于计算数学表达式的结果。

  • 计算函数calculateAnswer 方法接受一个数学表达式字符串,解析并计算表达式的结果。
  • 解析函数parseOperand 方法解析操作数,applyOp 方法应用运算符进行计算。
  • 运算符优先级precedence 方法确定运算符的优先级,以正确解析表达式。

FileUtil.java

这个类提供了基本的文件读写功能。

  • 读取函数readFromFile 方法从给定的文件路径中读取内容,并返回一个字符串列表。
  • 写入函数saveToFile 方法将给定的字符串列表写入到指定的文件路径中。

ExerciseGenerator.java

这个类负责生成四则运算题目和答案。

  • 生成题目generateExercises 方法生成指定数量的四则运算题目,并将题目和答案保存到文件中。
  • 生成单个题目generateExercise 方法生成单个四则运算题目,确保题目不重复且有效。
  • 生成操作数generateOperand 方法生成单个操作数(自然数或分数)。
  • 生成运算符generateOperator 方法随机生成一个运算符(加、减、乘、除)。
  • 构建表达式buildExpression 方法将操作数和运算符组合成一个完整的数学表达式。

MyApp.java

这个类是程序的入口点,负责解析命令行参数并调用相应的功能。

  • 主函数main 方法检查命令行参数,并根据参数调用 ExerciseGenerator 类生成题目和答案,或调用 GradeChecker 类对给定的题目文件和答案文件进行评分。

类与函数关系展示

MyApp
├── main(args: String[])
│   ├── -n 参数 -> ExerciseGenerator.generateExercises(num: int, range: int)
│   └── -e 参数 -> GradeChecker.gradeExercises(exerciseFile: String, answerFile: String)

ExerciseGenerator
├── generateExercises(num: int, range: int)
│   ├── generateExercise(range: int)
│   │   ├── generateOperand(range: int)
│   │   ├── generateOperator()
│   │   └── buildExpression(operands: List<String>, operators: List<String>)
│   └── ArithmeticCalculator.calculateAnswer(expression: String)
├── FileUtil.saveToFile(filename: String, content: List<String>)

GradeChecker
├── gradeExercises(exerciseFile: String, answerFile: String)
│   ├── FileUtil.readFromFile(filename: String)
│   └── ArithmeticCalculator.calculateAnswer(expression: String)
├── FileUtil.saveToFile(filename: String, content: List<String>)

ArithmeticCalculator
├── calculateAnswer(expression: String)
│   ├── parseOperand(operand: String)
│   ├── applyOp(op: String, b: Fraction, a: Fraction)
│   ├── precedence(op: String)
│   ├── isOperand(token: String)
│   └── isOperator(token: String)

FileUtil
├── readFromFile(filename: String)
└── saveToFile(filename: String, content: List<String>)

Fraction
├── add(other: Fraction)
├── subtract(other: Fraction)
├── multiply(other: Fraction)
├── divide(other: Fraction)
└── toProperFractionString()

这些类协同工作以实现题目生成和评分的完整流程。

2.关键函数展示

🍁ArithmeticCalculator.calculateAnswer 函数

主要功能:计算给定数学表达式的结果,并返回计算结果的字符串表示,这个函数是程序的核心部分之一,直接负责解析和计算数学表达式,其中构建了逆波兰表示法(RPN),通过使用两个栈(一个用于存储操作数,另一个用于存储运算符),将中缀表达式转换为后缀表达式,这种转换是简化了表达式的计算过程。
逆波兰表示法(Reverse Polish notation,RPN),又称后缀表示法,是一种由波兰数学家扬·武卡谢维奇于1920年引入的数学表达式方式。在这种表示法中,所有操作符都置于操作数的后面,因此得名“后缀表示法”。例如,中缀表达式“3 + 4”在逆波兰表示法中写作“3 4 +”,而“3 - 4 + 5”则写作“3 4 - 5 +”。
其特点如下:
无需括号:逆波兰表示法不需要括号来标识操作符的优先级,因为操作符的位置已经明确了运算顺序。
易于计算:其求值过程非常适合用堆栈结构实现。操作数依次入栈,遇到操作符时,从栈中弹出所需的操作数进行计算,结果再入栈。最终,栈顶的值即为表达式的计算结果。
紧凑性:逆波兰表达式通常比中缀表达式更紧凑,需要的存储空间更小。

具体代码实现:

public static String calculateAnswer(String expression) {
        expression = expression.replace("=", "").replace("÷", "/"); // 将“÷”替换回“/”以便计算
        String[] tokens = expression.split("\\s+");

        Stack<Fraction> values = new Stack<>();
        Stack<String> operators = new Stack<>();

        for (String token : tokens) {
            if (!token.isEmpty()) {
                if (isOperand(token)) {
                    values.push(parseOperand(token));
                } else if (isOperator(token)) {
                    while (!operators.isEmpty() && precedence(token) <= precedence(operators.peek())) {
                        values.push(applyOp(operators.pop(), values.pop(), values.pop()));
                    }
                    operators.push(token);
                }
            }
        }

        while (!operators.isEmpty()) {
            values.push(applyOp(operators.pop(), values.pop(), values.pop()));
        }

        return values.pop().toProperFractionString();
    }

函数流程图

🍁ExerciseGenerator.generateExercises 函数

主要功能:主要负责生成指定数量的四则运算题目,以达到题目要求的题目生成数量,这个函数确保生成的题目符合要求,不重复,并且每个题目都有相应的正确答案。
具体代码实现:

public static void generateExercises(int num, int range) {
        List<String> exercises = new ArrayList<>(num);
        List<String> answers = new ArrayList<>(num);

        for (int i = 0; i < num; i++) {
            String exercise = generateExercise(range);
            exercises.add(exercise);
            answers.add(ArithmeticCalculator.calculateAnswer(exercise.substring(0, exercise.length() - 2))); // Remove " ="
        }

        FileUtil.saveToFile("Exercises.txt", exercises);
        FileUtil.saveToFile("Answers.txt", answers);
    }

    private static String generateExercise(int range) {
        int numOperands = random.nextInt(3) + 1; // 随机选择1, 2或3个操作数
        List<String> operands = new ArrayList<>(numOperands);
        List<String> operators = new ArrayList<>(numOperands - 1);

        for (int i = 0; i < numOperands; i++) {
            operands.add(generateOperand(range));
        }

        for (int i = 0; i < numOperands - 1; i++) {
            operators.add(generateOperator());
        }

        return buildExpression(operands, operators);
    }

函数流程图

五、性能分析

1.性能分析图


2.性能瓶颈

  • 由于 ArithmeticCalculator.calculateAnswer 方法被频繁调用,这可能是性能瓶颈之一。
  • 频繁的分数运算和解析可能是导致性能问题的原因之一。

3.改进思路

🙋优化分数运算

  • 检查 Fraction 类的运算实现,确保没有不必要的对象创建或复杂的计算。
  • 考虑使用更高效的算法或数据结构来处理分数运算。

🙋减少重复计算

  • 检查是否有重复计算相同表达式的情况,尝试缓存和重用计算结果。

🙋简化表达式解析

  • 优化 ArithmeticCalculator 类的表达式解析逻辑,减少不必要的解析步骤。

4.程序中消耗最大的函数

  • 根据提供的分析图,程序中消耗最大的函数是 MyApp.main 方法,因为它是程序的入口点,并且调用了其他主要函数。

六、部分单元测试展示

1.测试代码

🍔分数计算类测试

🍿测试分数构造

@Test
    //测试分数构造
    void testFractionConstructor() {
        Fraction fraction = new Fraction(new BigInteger("1"), new BigInteger("2"));
        assertEquals("1/2", fraction.toProperFractionString());
    }

🍿测试分母为零时抛出异常

@Test
    //测试分母为零时抛出异常
    void testFractionConstructorWithZeroDenominator() {
        assertThrows(ArithmeticException.class, () -> new Fraction(new BigInteger("1"), new BigInteger("0")));
    }

🍿测试分数加减乘除

@Test
    //测试分数加法
    void testAdd() {
        Fraction f1 = new Fraction(new BigInteger("1"), new BigInteger("2"));
        Fraction f2 = new Fraction(new BigInteger("1"), new BigInteger("3"));
        assertEquals("5/6", f1.add(f2).toProperFractionString());
    }

@Test
    //测试分数减法
    void testSubtract() {
        Fraction f1 = new Fraction(new BigInteger("1"), new BigInteger("2"));
        Fraction f2 = new Fraction(new BigInteger("1"), new BigInteger("3"));
        assertEquals("1/6", f1.subtract(f2).toProperFractionString());
    }

@Test
    //测试分数乘法
    void testMultiply() {
        Fraction f1 = new Fraction(new BigInteger("1"), new BigInteger("2"));
        Fraction f2 = new Fraction(new BigInteger("1"), new BigInteger("3"));
        assertEquals("1/6", f1.multiply(f2).toProperFractionString());
    }

@Test
    //测试分数除法
    void testDivide() {
        Fraction f1 = new Fraction(new BigInteger("1"), new BigInteger("2"));
        Fraction f2 = new Fraction(new BigInteger("1"), new BigInteger("3"));
        assertEquals("1’1/2", f1.divide(f2).toProperFractionString());
    }

🍿测试分数转换为带分数字符串

@Test
    //测试分数转换为带分数字符串
    void testToProperFractionString() {
        Fraction f1 = new Fraction(new BigInteger("5"), new BigInteger("2"));
        assertEquals("2’1/2", f1.toProperFractionString());
    }

🍔算术计算器测试类

🍿测试简单算术表达式计算

@Test
    //测试简单算术表达式计算
    void testCalculateAnswerSimple() {
        assertEquals("3", ArithmeticCalculator.calculateAnswer("1 + 2"));
    }

🍿测试复杂算术表达式计算

@Test
    //测试复杂算术表达式计算
    void testCalculateAnswerComplex() {
        assertEquals("7", ArithmeticCalculator.calculateAnswer("1 + 2 * 3"));
    }

🍿测试分数算术表达式计算

@Test
    //测试分数算术表达式计算
    void testCalculateAnswerFraction() {
        assertEquals("5/6", ArithmeticCalculator.calculateAnswer("1/2 + 1/3"));
    }

🍔练习生成器测试类

🍿测试生成练习题

@Test
    //测试生成练习题
    void testGenerateExercises() {
        ExerciseGenerator.generateExercises(5, 10);
        List<String> exercises = FileUtil.readFromFile("Exercises.txt");
        List<String> answers = FileUtil.readFromFile("Answers.txt");
        assertEquals(5, exercises.size());
        assertEquals(5, answers.size());
    }

🍔答案检查测试类

🍿测试批改练习题

@Test
    //测试批改练习题
    void testGradeExercises() {
        List<String> exercises = List.of("1 + 2 = ", "3 * 4 = ");
        List<String> answers = List.of("3", "12");
        FileUtil.saveToFile("Exercises.txt", exercises);
        FileUtil.saveToFile("Answers.txt", answers);
        GradeChecker.gradeExercises("Exercises.txt", "Answers.txt");
        List<String> grade = FileUtil.readFromFile("Grade.txt");
        assertTrue(grade.get(0).contains("Correct: 2"));
    }

🍔文件测试类

🍿测试读取文件内容

@Test
    //测试读取文件内容
    void testReadFromFile() {
        List<String> content = FileUtil.readFromFile("testFile.txt");
        assertEquals(List.of("Line 1", "Line 2"), content);
    }

🍿测试读取不存在的文件

 @Test
    //测试读取不存在的文件
    void testReadFromFileNonExistent() {
        List<String> content = FileUtil.readFromFile("nonExistentFile.txt");
        assertTrue(content.isEmpty());
    }

🍿测试保存文件内容

@Test
    //测试保存文件内容
    void testSaveToFile() {
        List<String> content = List.of("Line 1", "Line 2");
        FileUtil.saveToFile("testFile.txt", content);
        List<String> readContent = FileUtil.readFromFile("testFile.txt");
        assertEquals(content, readContent);
    }

🍔主函数测试类

🍿测试无参数时输出用法信息

@Test
        // 测试无参数时输出用法信息
    void testMain_NoArguments() {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
        MyApp.main(new String[]{});
        String expectedOutput = "Usage: java MyApp -n <number> -r <range> "+ System.lineSeparator() +
                "       java MyApp -e <exercisefile>.txt -a <answerfile>.txt" + System.lineSeparator();
        assertEquals(expectedOutput, outContent.toString());
    }

🍿测试无效参数时输出错误信息

@Test
        // 测试无效参数时输出错误信息
    void testMain_InvalidArguments() {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
        MyApp.main(new String[]{"-x"});
        assertEquals("Invalid arguments." + System.lineSeparator(), outContent.toString());
    }

🍿测试生成练习题时参数不完整时输出错误信息

@Test
        // 测试生成练习题时参数不完整时输出错误信息
    void testMain_InvalidArgumentsForGeneratingExercises() {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
        MyApp.main(new String[]{"-n", "10"});
        assertEquals("Invalid arguments for generating exercises." + System.lineSeparator(), outContent.toString());
    }

🍿测试批改练习题时参数不完整时输出错误信息

@Test
        // 测试批改练习题时参数不完整时输出错误信息
    void testMain_InvalidArgumentsForGrading() {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
        MyApp.main(new String[]{"-e", "exercises.txt"});
        assertEquals("Invalid arguments for grading." + System.lineSeparator(), outContent.toString());
    }

🍿测试生成练习题的主方法

@Test
    //测试生成练习题的主方法
    void testMain_GenerateExercises() {
        MyApp.main(new String[]{"-n", "10", "-r", "100"});
    }

🍿测试批改练习题的主方法

 @Test
    //测试批改练习题的主方法
    void testMain_GradeExercises() {
        MyApp.main(new String[]{"-e", "exercises.txt", "-a", "answers.txt"});
    }

2.测试结果

posted on 2025-03-18 16:16  GeiFei  阅读(76)  评论(0)    收藏  举报

导航