双人合作项目-四则运算生成器
| 这个作业属于哪个课程 | 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 |
三、题目需求
题目:实现一个自动生成小学四则运算题目的命令行程序
实现一个生成小学四则运算题目的程序,支持命令行或图像界面,功能如下:
-
生成题目:
- 使用
-n参数控制题目数量,如-n 10生成10道题。 - 使用
-r参数控制数值范围,如-r 10生成10以内的题目,该参数必填。 - 题目要求:
- 计算过程不产生负数。
- 除法结果为真分数。
- 每题运算符不超过3个。
- 题目不重复。
- 题目和答案分别保存到当前目录的
Exercises.txt和Answers.txt文件中,真分数格式为3/5或2'3/8。
- 使用
-
批改题目:
- 使用
-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.测试结果

浙公网安备 33010602011771号