| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 结对项目——小学四则运算 |
| 这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
一、基本信息
1.1 队伍成员
| 姓名 | 学号 |
|---|---|
| 马佳纯 | 3221005026 |
| 肖依敏 | 3221005027 |
1.2 Github项目地址
二、项目需求
-
根据用户输入的参数 n 自动生成对应数量的四则运算题目(程序须支持一万道题目的生成)
-
根据用户输入的参数 r 控制题目中数值的范围
-
题目的运算结果不能为负数、假分数
-
每道题目中出现的运算符个数不超过3个
-
程序一次运行生成的题目不能重复,且将生成的题目存入Exercises.txt文件中,将题目的正确答案存入Answers.txt文件中
-
程序应能判定答案中的对错并进行数量统计,并将其结果写入Grade.txt文件中
三、PSP表格
| PSP2.1 | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 40 |
| ·Estimate | ·估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 600 | 700 |
| ·Analysis | ·需求分析 (包括学习新技术) | 120 | 160 |
| ·Design Spec | ·生成设计文档 | 60 | 50 |
| ·Design Review | ·设计复审 | 40 | 30 |
| ·Coding Standard | ·代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
| ·Design | ·具体设计 | 100 | 180 |
| ·Coding | ·具体编码 | 600 | 600 |
| ·Code Review | ·代码复审 | 40 | 40 |
| ·Test | ·测试(自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting | 报告 | 120 | 120 |
| ·Test Report | ·测试报告 | 60 | 30 |
| ·Size Measurement | ·计算工作量 | 30 | 15 |
| ·Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 40 | 20 |
| 合计 | 1920 | 2055 |
四、效能分析
4.1 程序生成一道题目时的性能分析


可以看出字符串拼接占用的内存空间最大
4.2 程序生成一万道题目时的性能分析


4.3 批改功能的性能分析


五、设计实现过程
5.1 程序流程图

5.2 主要的类

六、代码说明
6.1 题目容器类QuestionContainer
点击查看代码
public class QuestionContainer {
/** 生成指定数目的题目并存入容器 */
public void loadQuestions() {
// 直到存满
while(this.question2Ans.size() < this.capcity) {
// 生成一道题目
Question question = new Question(this.range);
question.generateQuestion();
// 获取题目的题目字符串和答案
String qStr = question.getqStr().toString();
Type ans = question.getAns();
// 对于两数算式如 1 + 2 = 与 2 + 1 = 重复情况进行特殊查重处理
Boolean isRepeat = false;
if(qStr.length() == 7) {
char a = qStr.charAt(0), b = qStr.charAt(4);
for (String str : question2Ans.keySet()) {
if(str.length() == 7 && (
(a == str.charAt(0) && b == str.charAt(4)) ||
(b == str.charAt(0) && a == str.charAt(4))
)) {
isRepeat = true;
break;
}
}
}
if(isRepeat) continue;
// 其他的若题目字符串相同则表示重复,在HashMap中会自动覆盖重复的题
this.question2Ans.put(qStr, ans);
}
}
}
分析:这段代码是一个题目容器类,用于生成指定数目的题目并存入容器。loadQuestions()方法使用一个while循环来判断题目容器是否已满,如果未满则继续生成题目。在每次循环中,首先创建一个Question对象,并调用其generateQuestion()方法生成一道题目。然后获取题目的题目字符串和答案,并进行特殊查重处理。
6.2 题目类Question
点击查看代码
public class Question {
/** 生成一道题目 */
public void generateQuestion() {
// 数字数量 随机: 2 3 4
numsCount = random.nextInt(3) + 2;
switch (numsCount) {
case 2 : {
equation = 1;
// 随机生成两个数 a b
Type a = getRandomTypeNum(), b = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP op = nops.poll();
// 判断是否需要出现 a - b 且 a < b的情况
if(isNeedSwap(a, b, op)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, op, false);
// 计算答案
ans = twoNumEquationCal(a, b, op);
qStr.append(" ").append("=");
break;
}
case 3: {
// 算式2~4号
equation = random.nextInt(3)+2;
// 随机生成三个数 a b c
Type a = getRandomTypeNum(), b = getRandomTypeNum(), c = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP firstOP = nops.poll();
OP secondOP = nops.poll();
if(equation == 2 || equation == 3) { // (1 + 2) + 3 或 1 + (2 + 3)
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, firstOP, true);
// 计算中间结果 res
Type res = twoNumEquationCal(a, b, firstOP);
// 判断是否需要出现 res - c 且 res < c 的情况
boolean isSwap = isNeedSwap(res, c, secondOP);
// 判断c的类型进行相应操作
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap) {
// 若需交换则将c与第三个运算符插入到算式的前面
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
// 否则将c与第三个运算符插入到算式后面
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
ans = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
ans = twoNumEquationCal(res, cc, secondOP);
}
}
} else if(equation == 4) { // 1 + 2 + 3
// 优先级判断,({加为1, 减为2} - 1) / 2 后为0,{乘为3, 除为4}运算后为1
if((firstOP.getCode() - 1) / 2 >= (secondOP.getCode() - 1) / 2) {
// 第一个操作符的优先级高
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, firstOP, false);
Type res = twoNumEquationCal(a, b, firstOP);
boolean isSwap = isNeedSwap(res, c, secondOP);
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
ans = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
ans = twoNumEquationCal(res, cc, secondOP);
}
}
} else {
// 第二个操作符的优先级高
if(isNeedSwap(b, c, secondOP)) {
Type temp = b;
b = c;
c = temp;
}
// 生成算式字符串
generateEquationStr(b, c, secondOP, false);
Type res = twoNumEquationCal(b, c, secondOP);
boolean isSwap = isNeedSwap(a, res, firstOP);
if(a.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction aa = (Fraction) a;
if(isSwap) {
qStr.append(" ").append(firstOP.getSign()).append(" ");
qStr.append(aa.generateStr());
ans = twoNumEquationCal(res, a, firstOP);
} else {
qStr.insert(0, " ").insert(0, firstOP.getSign()).insert(0, " ");
qStr.insert(0, aa.generateStr());
ans = twoNumEquationCal(a, res, firstOP);
}
} else {
Ordinary aa = (Ordinary) a;
if(isSwap) {
qStr.append(" ").append(firstOP.getSign()).append(" ");
qStr.append(aa.getValue());
ans = twoNumEquationCal(res, a, firstOP);
} else {
qStr.insert(0, " ").insert(0, firstOP.getSign()).insert(0, " ");
qStr.insert(0, aa.getValue());
ans = twoNumEquationCal(a, res, firstOP);
}
}
}
}
qStr.append(" ").append("=");
break;
}
case 4: {
// 算式5~7号
equation = random.nextInt(3)+5;
// 随机生成四个数
Type a = getRandomTypeNum(), b = getRandomTypeNum(), c = getRandomTypeNum(), d = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP firstOP = nops.poll();
OP secondOP = nops.poll();
OP thirdOP = nops.poll();
if(equation == 5 || equation == 6 || equation == 7) {
// 如(1 + 2) + 3 + 4 或 1 + (2 + 3) + 4 或 1 + 2 + 3 + 4
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
};
// 生成算式字符串
generateEquationStr(a, b, firstOP, true);
Type res = twoNumEquationCal(a, b, firstOP);
if((secondOP.getCode() - 1) / 2 >= (thirdOP.getCode() - 1) / 2) {
Boolean isSwap1 = isNeedSwap(res, c, secondOP);
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap1) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
res = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
res = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap1) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
res = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
res = twoNumEquationCal(res, cc, secondOP);
}
}
Boolean isSwap2 = isNeedSwap(res, d, thirdOP);
if(d.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction dd = (Fraction) d;
if(isSwap2) {
qStr.insert(0, " ").insert(0, thirdOP.getSign()).insert(0, " ");
qStr.insert(0, dd.generateStr());
ans = twoNumEquationCal(dd, res, thirdOP);
} else {
qStr.append(" ").append(thirdOP.getSign()).append(" ");
qStr.append(dd.generateStr());
ans = twoNumEquationCal(res, dd, thirdOP);
}
} else {
Ordinary dd = (Ordinary) d;
if(isSwap2) {
qStr.insert(0," ").insert(0, thirdOP.getSign()).insert(0, " ");
qStr.insert(0, dd.getValue());
ans = twoNumEquationCal(dd, res, thirdOP);
} else {
qStr.append(" ").append(thirdOP.getSign()).append(" ");
qStr.append(dd.getValue());
ans = twoNumEquationCal(res, dd, thirdOP);
}
}
} else {
if(isNeedSwap(c, d, thirdOP)) {
Type temp = c;
c = d;
d = temp;
};
Type res1 = twoNumEquationCal(c, d, thirdOP);
Boolean needSwap = isNeedSwap(res, res1, secondOP);
if(needSwap) {
String temp = qStr.toString();
qStr = new StringBuilder();
generateEquationStr(c, d, thirdOP, false);
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(temp);
ans = twoNumEquationCal(res1, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
// 生成算式字符串
generateEquationStr(c, d, thirdOP, false);
ans = twoNumEquationCal(res, res1, secondOP);
}
}
}
qStr.append(" ").append("=");
break;
}
}
}
}
分析:这段代码是根据生成的数字数量(2、3、4)来生成不同类型的数学题目。根据不同的情况,生成不同数量的随机数,并随机生成操作符。然后根据操作符的优先级和是否需要交换数字的顺序,生成相应的算式字符串。最后计算出答案并将其添加到算式字符串中。
七、测试运行
程序运行后会显示出一个功能菜单,供用户选择所需的功能,我们对三个功能都进行了一遍测试,测试结果与预期结果相符。

(1)当用户输入序号1时,程序执行自动生成题目功能,由上图可见,我们将生成10道四则运算题目,并且题目中数值的取值范围为1~10,运行结果如下图所示:

分析:程序随机生成了10道四则运算题目,并且过程中不会出现题目重复的情况,同时也避免了运算符个数超过3个的情况,符合项目要求,Exercises.txt文件的预期结果与实际结果相符。

分析:Answers.txt文件中的10个答案均为Exercises.txt文件中10个题目的正确结果,其中避免了运算结果可能为负数的情况,同时也未出现当题目中有除数时结果为假分数的情况,符合项目要求,预期结果与实际结果相符。
(2)当用户输入序号2时,程序执行批改题目功能,程序会要求用户输入所批改的题目文本的绝对路径以及与题目文本相对应的答案文本的绝对路径,最后将批改结果保存到Grade.txt文本中。

分析:这里为了测试方便以及符合作业要求,我们复制了上一环节所生成的10个四则运算题目,并且只在第5和第9个题目后面填写了正确的答案,将内容保存在了测试文本test.txt中。

上图为测试文件中10道题目的答案,保存在answer.txt文本中。

分析:由上图可知,Grade.txt文本的内容指出测试文本里面的题目有2道计算正确(编号分别为5,9),有8道计算错误(编号分别为1,2,3,4,6,7,8,10),预期结果与实际结果相符。
(3)当用户输入序号3时,执行退出程序功能,结束循坏,退出程序,预期结果与实际结果相符。
(4)测试程序是否能生成一万道题目




八、项目小结
-
马佳纯:这是我第一次跟别人合作完成一个项目。刚看到题目的时候以为是一个很简单的小项目,等到实际动手的时候却发现有很多困难,有很多东西需要考虑得更周全程序才不会出错。这次也幸亏我的合作伙伴对我的鼓舞,每次遇到疑点我们都会一起讨论解决,相比起之前自己一个人解决bug效率提高了许多。不仅如此,我也进一步加深理解了开发一个项目的具体流程,更加明白了团队间的互帮互助对一个项目的重要性。
-
肖依敏:在项目开始前,我与搭档就如何实现这个四则运算生成与批改程序进行了几番讨论,并形成了基本的框架,主要类有数值类Type,题目类Question,题目容器类QuestionContainer, 常量枚举类NumType和OP,文件工具类FileUtil,计算类Calculator以及程序入口类Application。接着我们就这个基本框架开始进行编码,逐步实现并完善各个类,当然过程中也出现了一些bug,比如批改作业时忘记去掉答案前面的空格符导致每次批改都显示正确题数为0,但是最后经过我和搭档的探讨以及百度类似情况如何解决,我们最终成功除掉了那些bug,完成了这个四则运算生成与批改程序。通过这次作业,我体会到了团队合作编码的重要性,对方可以注意到我忽略的一些点,从而进行改正,有利于提高自身的能力。