结对项目
这个作业属于哪个课程 | [计科22级34班 ]——https://edu.cnblogs.com/campus/gdgy/CSGrade22-34 |
---|---|
这个作业要求在哪里 | [结对项目]——https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230 |
这个作业的目标 | 使用Java完成四则运算生成器,在实现项目的过程中和运用软件工程的知识进行学习开发,学会使用PSP表格,使用github提交项目,进行单元测试与问题处理。 |
成员 | 3122004531李晓彬 3122004539谭传兴 |
[个人项目github仓库]——https://github.com/aBin-L1/-
一、题目:
实现一个自动生成小学四则运算题目的命令行程序,同时支持图像界面,功能与命令行程序相似。
-
算术表达式与符号定义:
- 自然数:如0, 1, 2, …。
- 真分数:如1/2, 1/3, 2/3, 1/4, 1’1/2等。
- 运算符:+, −, ×, ÷。
- 括号:圆括号 ( )。
- 等号:=。
- 分隔符:空格(用于运算符和等号前后)。
-
四则运算题目格式:
- 题目形式:e = ,其中e为算术表达式。
- 表达式规则:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e)
,其中n为自然数或真分数,e1、e2为表达式。
-
程序功能要求:
- 生成题目个数:使用
-n
参数控制生成题目的数量。例如:Myapp.exe -n 10
表示生成10个题目。 - 数值范围控制:使用
-r
参数控制题目中数值的范围(自然数、真分数和真分数分母)。例如:Myapp.exe -r 10
表示生成数值在10以内(不包括10)的四则运算题目。该参数必须提供,否则程序报错并给出帮助信息。 - 计算过程约束:
- 不允许产生负数:e1 - e2 中应保证 e1 ≥ e2。
- 除法结果要求为真分数:e1 ÷ e2 的结果应为真分数。
- 每道题目的运算符个数不超过3个。
- 题目去重:
- 程序生成的题目不能重复。对于相同运算顺序的题目,即使交换了运算符的位置,依然视为重复。
- 特殊情况下,如加法结合律下,1 + 2 + 3 和 3 + 2 + 1 视为不同题目。
- 输出:
- 生成的题目存储在
Exercises.txt
文件中。 - 题目答案存储在
Answers.txt
文件中。
- 生成的题目存储在
- 生成题目个数:使用
-
特殊处理:
- 真分数的输入输出格式:如真分数五分之三表示为3/5,二又八分之三表示为2’3/8。
- 真分数运算的示例:1/6 + 1/8 = 7/24。
-
扩展功能:
-
程序能支持生成一万道题目。
-
答案验证与统计:对给定的题目和答案文件进行答案正确性的判定,并进行统计。
使用参数
-e <> -a <>
分别指定题目文件和答案文件。例如:
Myapp.exe -e exercises.txt -a answers.txt
- 统计结果输出到
Grade.txt
文件中,格式为:Correct: 5 (1, 3, 5, 7, 9)
和Wrong: 5 (2, 4, 6, 8, 10)
。
- 统计结果输出到
-
二、PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 200 | 300 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 | 60 | 45 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 60 | 66 |
· Coding | · 具体编码 | 30 | 60 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 150 |
Reporting | 报告 | 60 | 60 |
· Test Repor | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 20 | 30 |
· Postmorte· 合计m & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 60 |
· 合计 | 980 | 1171 |
三、设计实现过程
1.程序功能需求分析
-
生成算术题目:程序应能够生成指定数量的算术题目,题目的操作数可以是自然数、真分数或带分数,运算符包括加法、减法、乘法和除法。
-
检查答案:程序应能够检查用户提供的答案文件,并将检查结果记录在文件中。
-
文件管理:程序需要读写多个文件,包括存储题目、答案和检查结果的文件。
2.主要功能模块划分
-
主程序模块(Main 类):负责解析命令行参数,并调用相应的功能。
-
题目生成模块(ProblemGenerator 类):生成随机算术题并将其存储到文件中。
-
表达式求值模块(ExpressionEvaluator 类):负责计算生成的算术表达式的值。
-
分数处理模块(Fraction 类):用于处理和运算涉及分数的算术表达式。
-
文件管理模块(FileManager 类):负责与文件系统的交互,处理文件的读写操作。
-
答案检查模块(AnswerChecker 类):检查用户提交的答案是否正确。
3. 类的设计与实现
3.1 主程序模块(Main 类)
- 职责:解析命令行参数,根据用户的输入调用不同的功能模块。
- 主要方法:
main(String[] args)
:主入口方法,负责解析命令行参数,并根据参数调用题目生成或答案检查功能。showUsage()
:辅助方法,用于当用户输入参数有误时,展示正确的使用方法。
3.2 题目生成模块(ProblemGenerator 类)
- 职责:生成指定数量的随机算术题,确保题目不重复且结果有效(非负)。
- 主要方法:
generateProblems()
:生成指定数量的题目,并将其写入文件。generateRandomProblem(Random random)
:生成随机算术题。generateSubExpression(Random random, int numOperators)
:生成含有多个操作数和运算符的子表达式。generateRandomOperand(Random random)
:生成随机操作数,包括自然数、真分数和带分数。generateRandomOperator(Random random)
:随机生成运算符。simplifyFraction(int numerator, int denominator)
:化简分数。gcd(int a, int b)
:计算最大公约数,用于分数化简。
3.3 表达式求值模块(ExpressionEvaluator 类)
- 职责:将中缀表达式转换为后缀表达式,并计算表达式的结果。
- 主要方法:
evaluate(String expression)
:评估表达式,返回结果。infixToPostfix(String infix)
:将中缀表达式转换为后缀表达式。evaluatePostfix(String postfix)
:计算后缀表达式的值。isOperator(char c)
:判断字符是否为运算符。precedence(char c)
:确定运算符的优先级。calculate(Fraction a, Fraction b, char operator)
:根据运算符计算两个分数的运算结果。
3.4 分数处理模块(Fraction 类)
- 职责:表示分数,并提供分数的四则运算。
- 主要方法:
Fraction(int numerator, int denominator)
:构造函数,创建分数并化简。fromString(String str)
:从字符串解析分数,包括带分数和真分数。add(Fraction other)
:分数加法。subtract(Fraction other)
:分数减法。multiply(Fraction other)
:分数乘法。divide(Fraction other)
:分数除法。gcd(int a, int b)
:计算最大公约数,用于分数化简。toString()
:将分数转换为字符串表示,可能是整数、真分数或带分数形式。
3.5 文件管理模块(FileManager 类)
- 职责:管理与文件系统的交互,处理题目、答案和成绩的读写操作。
- 主要方法:
clearFile(String fileName)
:清空指定文件的内容。writeProblem(String problem)
:将题目写入题目文件。writeAnswer(String answer)
:将答案写入答案文件。writeGrade(List<Integer> correct, List<Integer> wrong)
:将成绩写入成绩文件。readFile(String fileName)
:读取指定文件的内容。
3.6 答案检查模块(AnswerChecker 类)
- 职责:检查用户提交的答案文件是否正确,并将检查结果写入文件。
- 主要方法:
checkAnswers()
:对比用户提供的答案与程序计算的正确答案,记录正确和错误的题目编号。readFile(String fileName)
:读取文件内容,用于加载题目和答案。
4. 类之间的关系
- Main 类:作为程序的入口,解析命令行参数,决定调用
ProblemGenerator
还是AnswerChecker
。 - ProblemGenerator 类:负责生成题目,并通过
FileManager
将题目和答案写入文件,同时依赖ExpressionEvaluator
评估题目的正确答案。 - ExpressionEvaluator 类:作为独立的工具类,为
ProblemGenerator
和AnswerChecker
提供表达式求值功能。 - Fraction 类:为
ExpressionEvaluator
提供基础分数计算支持。 - FileManager 类:独立管理文件读写操作,为
ProblemGenerator
和AnswerChecker
提供服务。 - AnswerChecker 类:依赖
FileManager
读取题目和答案文件,依赖ExpressionEvaluator
评估用户提供的答案。
五、代码说明
1.ProblemGenerator类中的生成子表达式方法
private String generateSubExpression(Random random, int numOperators) {
List<String> operands = new ArrayList<>();
List<String> operators = new ArrayList<>();
// 生成第一个操作数并初始化当前结果
String currentOperand = generateRandomOperand(random);
double currentResult = evaluateOperand(currentOperand);
operands.add(currentOperand);
for (int i = 0; i < numOperators; i++) {
String operator = generateRandomOperator(random);
String nextOperand = generateRandomOperand(random);
double nextValue = evaluateOperand(nextOperand);
// 调整运算符和操作数以确保不产生负数
if (operator.equals("-")) {
if (currentResult < nextValue) {
// 如果减法会导致负数,交换操作数和符号
operator = "+";
}
} else if (operator.equals("/")) {
if (nextValue == 0 || currentResult < nextValue) {
// 确保除数不为零,且避免分子小于分母导致结果小于1
operator = "*";
}
}
// 更新当前结果
currentResult = calculateNewResult(currentResult, nextValue, operator);
operators.add(operator);
operands.add(nextOperand);
}
// 随机决定是否将某些操作数和运算符包围在括号中,确保括号不包围整个表达式
if (numOperators > 1) {
int startIdx = random.nextInt(numOperators); // 括号的起始位置
int endIdx = startIdx + random.nextInt(numOperators - startIdx) + 1; // 括号的结束位置
if (startIdx > 0 || endIdx < numOperators) {
operands.set(startIdx, "(" + operands.get(startIdx));
operands.set(endIdx, operands.get(endIdx) + ")");
}
}
// 拼接表达式
StringBuilder subExpression = new StringBuilder();
for (int i = 0; i < numOperators; i++) {
subExpression.append(operands.get(i)).append(" ").append(operators.get(i)).append(" ");
}
subExpression.append(operands.get(numOperators));
return subExpression.toString();
}
操作数和运算符:首先,生成第一个操作数,并将其存入 operands
列表。然后,循环生成剩余的运算符和操作数。生成每个操作数后,立即评估其数值,并根据数值判断是否需要调整运算符,以防止出现负数或无效除法(如除以零)。
运算符调整:对于减法操作,如果当前结果小于下一个操作数,则将减法改为加法,避免产生负数。同样地,对于除法操作,如果除数为零或当前结果小于下一个操作数,则将除法改为乘法,避免无效结果。
括号处理:为了增加表达式的复杂性,随机决定是否在表达式的某部分添加括号。这通过随机生成的起始和结束位置来实现,但括号不会包围整个表达式。
表达式拼接:最后,将生成的操作数和运算符拼接成一个完整的表达式。
2.ProblemGenerator类中的生成子表达式方法
private String generateRandomOperand(Random random) {
int type = random.nextInt(3); // 调整为3种类型:自然数、真分数、带分数
switch (type) {
case 0: // 生成自然数
return String.valueOf(random.nextInt(range) + 1);
case 1: // 生成真分数并化简
int numerator, denominator;
do {
numerator = random.nextInt(range - 1) + 1;
denominator = random.nextInt(range - 1) + 2; // 确保分母大于分子,且分母最小为2
} while (numerator >= denominator); // 重复生成直到分子小于分母
return simplifyFraction(numerator, denominator);
case 2: // 生成带分数并化简真分数部分
int wholeNumber = random.nextInt(range - 1) + 1; // 整数部分的最大值由 range 决定
do {
numerator = random.nextInt(range - 1) + 1;
denominator = random.nextInt(range - 1) + 2; // 确保分母大于分子,且分母最小为2
} while (numerator >= denominator); // 重复生成直到分子小于分母
String simplifiedFraction = simplifyFraction(numerator, denominator);
return wholeNumber + "'" + simplifiedFraction;
default:
return String.valueOf(random.nextInt(range) + 1);
}
}
生成三种类型的操作数:操作数可以是自然数、真分数或带分数。根据 random.nextInt(3)
的结果决定生成哪种类型。
- 自然数:生成范围内的随机自然数。
- 真分数:通过生成随机的分子和分母,并确保分子小于分母,同时对生成的分数进行化简。
- 带分数:生成一个随机的整数部分和一个真分数部分,并确保真分数部分是化简后的形式。
分数化简:调用 simplifyFraction
方法,将生成的分数进行化简,确保结果是最简形式。
3.ExpressionEvaluator类
import java.util.Stack;
public class ExpressionEvaluator {
public String evaluate(String expression) {
try {
// 将表达式转化为后缀表达式
String postfix = infixToPostfix(expression);
// 计算后缀表达式的值
String result = evaluatePostfix(postfix);
return result;
} catch (Exception e) {
return null;
}
}
private String infixToPostfix(String infix) {
// 将中缀表达式转化为后缀表达式
Stack<Character> stack = new Stack<>();
StringBuilder postfix = new StringBuilder();
for (char c : infix.toCharArray()) {
if (Character.isDigit(c) || c == '/' || c == '\'') {
postfix.append(c);
} else if (c == '(') {
stack.push(c);
} else if (c == ')') {
while (!stack.isEmpty() && stack.peek() != '(') {
postfix.append(' ').append(stack.pop());
}
stack.pop();
} else if (isOperator(c)) {
while (!stack.isEmpty() && precedence(c) <= precedence(stack.peek())) {
postfix.append(' ').append(stack.pop());
}
postfix.append(' ');
stack.push(c);
}
}
while (!stack.isEmpty()) {
postfix.append(' ').append(stack.pop());
}
return postfix.toString();
}
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private int precedence(char c) {
switch (c) {
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
default:
return -1;
}
}
private String evaluatePostfix(String postfix) {
// 计算后缀表达式的值
Stack<Fraction> stack = new Stack<>();
String[] tokens = postfix.split(" ");
for (String token : tokens) {
if (isOperator(token.charAt(0))) {
Fraction b = stack.pop();
Fraction a = stack.pop();
stack.push(calculate(a, b, token.charAt(0)));
} else {
stack.push(Fraction.fromString(token));
}
}
return stack.pop().toString();
}
private Fraction calculate(Fraction a, Fraction b, char operator) {
switch (operator) {
case '+':
return a.add(b);
case '-':
return a.subtract(b);
case '*':
return a.multiply(b);
case '/':
return a.divide(b);
default:
return new Fraction(0, 1);
}
}
}
ExpressionEvaluator
类是程序中的一个关键组件,用于处理和评估算术表达式的值。它的主要功能包括将中缀表达式转换为后缀表达式(逆波兰表达式),然后根据后缀表达式计算最终的结果。这个类的设计主要是为了支持算术题目的生成和答案检查功能。
该类的结构与主要方法
1. evaluate(String expression)
- 功能:这是
ExpressionEvaluator
类的核心方法,用于计算输入的算术表达式的值。 - 逻辑:
- 首先将中缀表达式转换为后缀表达式(使用
infixToPostfix()
方法)。 - 然后使用后缀表达式计算表达式的最终结果(使用
evaluatePostfix()
方法)。 - 如果表达式不合法或计算过程中出现错误,返回
null
表示计算失败。
- 首先将中缀表达式转换为后缀表达式(使用
2. infixToPostfix(String infix)
- 功能:将中缀表达式转换为后缀表达式。
- 逻辑:
- 使用一个栈来处理运算符的优先级和括号。
- 依次扫描表达式中的每个字符,将操作数直接添加到后缀表达式中,对于运算符则根据其优先级进行栈的操作。
- 确保括号内的运算符正确地按顺序出现在后缀表达式中。
- 在扫描完表达式后,将栈中剩余的运算符依次弹出并添加到后缀表达式。
3. evaluatePostfix(String postfix)
- 功能:根据后缀表达式计算表达式的值。
- 逻辑:
- 使用一个栈来处理操作数。
- 依次扫描后缀表达式的每个令牌(token),如果是操作数就压入栈中,如果是运算符则弹出栈顶的两个操作数进行计算。
- 计算结果再次压入栈中,直到表达式扫描完成,栈顶元素即为最终结果。
4. calculate(Fraction a, Fraction b, char operator)
- 功能:执行具体的加减乘除运算。
- 逻辑:
- 根据传入的运算符,对两个分数进行相应的运算操作。
- 使用
Fraction
类的加法、减法、乘法和除法方法,返回运算结果。
5. 辅助方法
isOperator(char c)
:判断一个字符是否为运算符(+
、-
、*
、/
)。precedence(char c)
:返回运算符的优先级。乘法和除法优先级较高,加法和减法优先级较低。
六、测试运行
- 1.使用命令
java -jar demo.jar -n 10 -r 10
Exercises.txt文件存放生成的表达式,表达式有1~3个运算符,算数有三种类型。
Answers.txt文件生成表达式的答案
- 2.使用命令
java -jar demo.jar -e Exercises.txt -a Answers.txt
题目对错的结果存入Grade.txt文件中,此时Answers.txt中的文件没有经过更改,答案是全对的。
现在将Answers.txt中的答案进行更改,可以看到更改后运行批改命令,Grade.txt生成相应的答案对错信息。
- 3.命令参数输入不正常,异常处理
- 4.生成10000到题目,运行的时候需要点时间
性能分析:
覆盖率:
生成1000道题目时性能状况。
七、项目小结
在这个结对项目中,我们的目标是开发一个能够自动生成和检查四则运算的命令行工具。项目初期,我们明确了每个人的角色分工,虽然一开始进展顺利,但很快我们就遇到了一个棘手的问题:在处理复杂的带分数运算时,表达式评估的准确性一直无法达到我们的预期。这个问题让我们犯了难,几乎让项目进度停滞。不过经过我们的讨论还是找到的解决的办法。这次作业让我们体会到了团队开发项目是一件不简单的事情,开发过程中需要进行有效的沟通才能推进项目。这次的作业也是收获满满,对团队开发的认识又多了一些。