结对项目
软件工程 | https://edu.cnblogs.com/campus/gdgy/InformationSecurity1912-Softwareengineering |
---|---|
作业要求 | https://edu.cnblogs.com/campus/gdgy/InformationSecurity1912-Softwareengineering/homework/12147 |
作业目标 | 实现一个自动生成小学四则运算题目的程序,并且能够对题目和学生答题的答案进行比较,判断学生的答题情况 |
项目成员1 | 3119005483 叶臻强 |
项目成员2 | 3119005464 黎梓洋 |
源码链接
已经发布在GitHub上:https://github.com/yzqctf/3119005483/tree/main/Arithmetic
PSP表格
PSP 各个阶段 | 自己预估的时间(分钟) | 实际的记录(分钟) |
---|---|---|
计划: 明确需求和其他因素,估计以下的各个任务需要多少时间 | 20 | 20 |
开发 (包括下面 8 项子任务) | (以下都填预估值) | 650 |
需求分析 (包括学习新技术) | 30 | 60 |
生成设计文档 | 15 | 15 |
设计复审 | 30 | 40 |
代码规范 | 60 | 40 |
具体设计 | 10 | 15 |
具体编码 | 420 | 300 |
代码复审 | 60 | 120 |
测试 | 30 | 60 |
报告 | 60 | 60 |
测试报告 | 30 | 30 |
计算工作量 (多少行代码,多少次签入,多少测试用例,其他工作量) | 30 | 30 |
事后总结, 并提出改进计划 (包括写文档、博客的时间) | 60 | 60 |
总共花费的时间 (分钟) | 855 | 850 |
需求分析
- 运算符个数不超过3个;
- 题目不能重复;
- 生产题目文件存在当前目录下的Exercises.txt;
- 计算出所有题目的答案并存到当前目录下的Answers.txt中;
- 能够支持10000道题目的生成;
- 判断答案的对错并进行数量统计
- 运算结果不能是负数;
- 除法的输出结果是真分数
- 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
- 使用 -n 参数控制生成题目的个数
通过上述需求不难分析出:程序需要接收四个参数-r, -n, Exercises path, Answers path, 分别对应表达式中数值的范围 ,Exercises.txt的路径和Answers.txt的路径,不能重复需要自己指定重复的标准;现规定如下:
- 结果是否已经出现过:如果没出现过,不会重复(这个条件已经能筛选出很多符合标准的答案)
- 如果结果已经出现过,只有完全相同才算重复,即可通过表达式的逆波兰表达式进行检查
具体实现
实现流程
- 生成表达式
- 整数
- 分数
- 带分数
- 带括号
- 计算
- 转化为逆波兰表达式
- 整数一套最简单的规则
- 分数:统一形式成 'a/b' 表示b分之a;注:'1'1/2':表示1又1/2,即3/2再计算
- 分数约分
- 找最大公因数
- 转化为带分数
- 返回符合格式的答案
- 判断题目是否重复
- 生成题目文件,答案文件
- 计算错题数,以及错题所在题号
key words:随机生成表达式,统一表达式,最大公因数约分,转化为合适的输出格式
具体代码实现
在本例中,使用三个类完成四则计算器的实现
其中GUI为图形化界面,所以主要实现还是由AlgoArithmetic与AlgoCalculate完成
生成表达式
//生成表达式,f表示是否生成带括号表达式
public static String finalFormula(boolean f){
String formula = new String();
Random random = new Random();
int numberOfOperation = random.nextInt(OPERATORNUMBER)+1;
if (numberOfOperation==1){
formula += getRandomNumber();
formula +=" " + getRandomOperator();
formula +=" " + getRandomNumber();
numberOfOperation--;
return formula;
}
if (!f){
//不需要括号
formula += getRandomNumber();
while (numberOfOperation!=0){
formula +=" " + getRandomOperator();
formula +=" " + getRandomNumber();
numberOfOperation--;
}
}else {
int first = random.nextInt(2);
// System.out.println("括号位置:"+first);
if (first==0){
formula += BRACKETS[0];
formula +=" "+ getRandomNumber();
formula +=" " + getRandomOperator();
formula +=" " + getRandomNumber();
formula +=" "+ BRACKETS[1];
formula +=" " + getRandomOperator();
formula +=" " + getRandomNumber();
}else if (first==1){
formula += getRandomNumber();
formula +=" " + getRandomOperator();
formula +=" "+BRACKETS[0];
formula +=" "+ getRandomNumber();
formula +=" " + getRandomOperator();
formula +=" " + getRandomNumber();
formula +=" "+ BRACKETS[1];
}
}
return formula;
}
//生成随机数(整数或真分数),范围可控,根据输入的-r进行判断
public static String getRandomNumber() {
//不能生成负数,所以简单了
String num = new String();
Random random = new Random();
if (random.nextInt(2)==0){
num = String.valueOf(random.nextInt(RANGEVALUE)+1);
}else {
//分子
int molecule = random.nextInt(RANGEVALUE)+1;
//分母
int denominator = random.nextInt(RANGEVALUE)+1+molecule;
num = molecule+"/"+denominator;
}
return num;
}
其中有全局变量,可以在GitHub上看源代码,都有着注释。
调用计算接口
//随机带括号的题目
int numOfBracketProblems = random.nextInt(NUMBER_PROBLEMS-1)+1;
System.out.println("带括号的题目数: " + numOfBracketProblems);
NUMBER_PROBLEMS -= numOfBracketProblems;
while (numOfBracketProblems!=0){
String formula = finalFormula(true);
// System.out.println(formula);
//是否重复
boolean ok = end(formula);
if (!ok)
continue;
//System.out.println(formula+" = ");
numOfBracketProblems--;
}
while (NUMBER_PROBLEMS!=0){
String formula = finalFormula(false);
// System.out.println(formula);
boolean ok = end(formula);
if (!ok)
continue;
//System.out.println(formula+" = ");
NUMBER_PROBLEMS--;
}
//返回布尔值,判断是否重复
public static boolean end(String formula){
String re = new AlgoCalculate(formula).result;
if (re==null){
re="0";
}
if (re.contains("-"))
return false;
//判断重复
if (out.containsKey(re))
return false;
FORMULA.add(formula);
out.put(re,formula);
return true;
}
计算AlgoCalculate
在AlgoCalculate类中具体实现了以下方法
下面放一些主要方法的源码
//将formula转化为逆波兰表达式,即后缀表达式
public static List<String> initRPN(String formula){
List<String> rpn = new ArrayList<>();
//存放操作符的栈
Stack<String> stack = new Stack();
String operation = "(+-*÷)";
// System.out.println(formula.length());
String[] split = formula.split(" ");
int length = split.length;
for (int i=0;i<length;i++){
String str = split[i];
if (isNumber(str)){
rpn.add(str);
}else {
if (str.equals("(")){
//直接入栈
stack.push(str);
}else if (str.equals(")")){
//出栈,直到遇到'('或者栈空
while (!stack.empty()){
String temp = stack.pop();
if (!temp.equals("(")){
//如果不是左括号
rpn.add(temp);
}else
break;
}
} else {
//如果此使栈为空了
if (stack.empty())
stack.push(str);
else {
//栈不空就需要进行运算发符比较
//compare
if (compare(stack.peek())>=compare(str)){
while (!stack.empty()&&compare(stack.peek())>=compare(str))
rpn.add(stack.pop());
}
stack.push(str);
}
}
}
}
while (!stack.empty())
rpn.add(stack.pop());
return rpn;
}
//具体的运算实现
//b 运算 a
public static String calculate(String a, String b, String symbol){
String result=null;
//是否存在分数:
boolean flag = false;
//统一形式
//1.带分数
if (a.contains("'")){
String[] str = a.split("'");
String[] str1 = str[1].split("/");
int numerator = Integer.parseInt(str[0])*Integer.parseInt(str1[1])+Integer.parseInt(str1[0]);
a = numerator+"/"+str1[1];
}
if (b.contains("'")){
String[] str = b.split("'");
String[] str1 =str[1].split("/");
int numerator = Integer.parseInt(str[0])*Integer.parseInt(str1[1])+Integer.parseInt(str1[0]);
b = numerator+"/"+str1[1];
}
if (a.contains("/")||b.contains("/")) flag=true;
if (flag){
boolean w = false;
if (a.contains("/")&&b.contains("/")) w=true;
//只有一个分数,转换格式;
if (!w){
if (a.contains("/")){
String[] x = a.split("/");
int numerator = Integer.parseInt(b)*Integer.parseInt(x[1]);
b = numerator+"/"+x[1];
}else if (b.contains("/")){
String[] x = b.split("/");
int numerator = Integer.parseInt(a)*Integer.parseInt(x[1]);
a = numerator+"/"+x[1];
}
}
}
if (flag){
int ans=0;
String[] x = a.split("/");
String[] x1 = b.split("/");
if (symbol.equals("*")){
//分母
int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
//分子
int numerator = Integer.parseInt(x[0])*Integer.parseInt(x1[0]);
if (denominator==0||numerator==0)
result="0";
else
result = reduction(denominator,numerator);
}
else if (symbol.equals("÷")){
//两分数相÷
int denominator = Integer.parseInt(x1[1])*Integer.parseInt(x[0]);
int numerator = Integer.parseInt(x1[0])*Integer.parseInt(x[1]);
if (denominator!=0&&numerator!=0)
result = reduction(denominator,numerator);
else
result = "0";
}else if (symbol.equals("+")){
int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
int numerator = Integer.parseInt(x[0])*Integer.parseInt(x1[1]) + Integer.parseInt(x1[0])*Integer.parseInt(x[1]);
if (denominator==0||numerator==0)
result="0";
else
result = reduction(denominator,numerator);
}else if (symbol.equals("-")){
int denominator = Integer.parseInt(x[1])*Integer.parseInt(x1[1]);
int numerator = Integer.parseInt(x1[0])*Integer.parseInt(x[1]) - Integer.parseInt(x1[1])*Integer.parseInt(x[0]);
if (numerator==0||denominator==0){
result="0";
}
else if (numerator<0)
result = numerator+"/"+denominator;
else
result = reduction(denominator,numerator);
}
}else {
int tempNumber1 = Integer.parseInt(a);
int tempNumber2 = Integer.parseInt(b);
int ans=0;
if (symbol.equals("+"))
ans = tempNumber2+tempNumber1;
else if (symbol.equals("-"))
ans = tempNumber2-tempNumber1;
else if (symbol.equals("*"))
ans = tempNumber2*tempNumber1;
else if (symbol.equals("÷")){
if (tempNumber1!=0){
if (tempNumber2%tempNumber1==0)
ans = tempNumber2/tempNumber1;
else {
return result = reduction(tempNumber1,tempNumber2);
}
}else {
ans=0;
}
}
result = String.valueOf(ans);
}
return result;
}
运行结果展示
- 题目文件
- 答案文件
- 运行界面
- 检查作业
通过对比发现,检查作业这一功能得以实现。
测试类
-
整体程序测试(不含图形化界面)
-
表达式计算
public void calculate(){
String re;
//加法
String f1 = "4 + 3 * 3/4";
re = new AlgoCalculate(f1).result;
System.out.println("加法:"+re);
//减法出现负数
String f2 = "3 - 4";
boolean ok = AlgoArithmetic.end(f2);
if (!ok) System.out.println("结果为负数,请重新输入");
//乘法
String f3 = "6 * ( 1/3 * 6 )";
re = new AlgoCalculate(f3).result;
System.out.println("乘法:"+re);
//除法
String f4 = "8/16 ÷ 6/10 ÷ 9/14 ÷ 9";
re = new AlgoCalculate(f4).result;
System.out.println("除法:"+re);
//除法分母为0,注:分母为0的条件下自定义结果为0,虽然可能是无穷大
String f5 = "10 ÷ ( 5 - 5 )";
re = new AlgoCalculate(f5).result;
System.out.println("分母为0:"+re);
}
- 出现未完成全部题目的情况
//学生未完成作业
@Test
public void falseWork() throws IOException {
AlgoArithmetic.check("D:\\学习资料\\softwareProject\\Arithmetic\\Answers.txt","D:\\学习资料\\softwareProject\\Arithmetic\\Exercises.txt");
}
这里测试是完全没答题的情况下:
程序性能展示
- 代码覆盖率
- 时间、内存使用情况
代码审查
码风问题在这里就不再重复修改了,会不断学习的!!
其中的三个erro prone为Test测试单元中没有写assert()或fail()
从结果来看,代码的问题较上次提交个人项目减少了4%,复杂度增加了4%,代码重复度从上次的17%到现在的20%,20%总体来说符合自主完成作业没有抄袭的要求。
总结
成员:叶臻强
- 一定要留够摸鱼时间!!!国庆写完了表达式生成,剩下的计算规则代码完成硬是拖到了最后一刻,虽然期间也有别的事情要处理,但是完成一个项目就按照最坏的情况去安排时间就是对的;
- 原本以为逆波兰表达式(后缀表达式)已经学过,上手驾轻就熟,但是不要高估自己的动手实践能力,总是会有奇奇怪怪的问题的,所以要尽早开始学习文档;
- 命名一定要做到“见字如面”,要不然写方法的时候会搞不清变量之间的关系,深刻例子:哪一个是减数,哪一个是被减数(同理除法);
- 同样的代码一定要提取公因式,减少整体的冗余,同样,处理问题的时候尽量把可能输入的问题进行统一格式,处理一种格式总比不断的分情况讨论简单;
- 结对项目最好的一点是各有所长,我做好我的部分之后,剩下的就不需要考虑了,除了解释怎么调用之外。
成员:黎梓洋
- 在本次项目中,我主要负责图形化界面的实现以及代码接口的测试;
- 修改一些bug,增强代码的健壮性;
- 在本次项目中知道了项目的分工以及合作的重要性;