结对编程--自动生成小学四则运算
小学四则运算题目生成
这个作业属于哪个课程 | 软件工程2024 |
---|---|
这个作业要求在哪里 | 结对项目 |
这个作业的目标 | 完成结对项目,共同合作实现自动生成小学四则运算题目 |
参与人员 | 温泽坤3122004582、黄浩3122004571 |
作业github地址
PSP2.1表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 620 | 1050 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 180 |
· Design Spec | · 生成设计文档 | 40 | 30 |
· Design Review | · 设计复审 | 30 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 300 | 600 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 120 |
Reporting | 报告 | 120 | 120 |
· Test Repor | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
· 合计 | 800 | 1230 |
一、需求分析
设计一个能够批量生成小学四则运算的程序,要求满足以下标准:
1、使用 -n 参数控制生成题目的个数。
分析:题目数量可以随参数改变而改变
2、使用 -r 参数控制题目中数值。
分析:题目数值的最大值可以随参数改变而改变
3、生成的题目中计算过程不能产生负数。
分析:要对减法进行额外判断处理
4、生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
分析:要对结果进行转化,使其满足真分数形式
5、每道题目中出现的运算符个数不超过3个。
分析:限制运算符数量和运算数数量,使前者最多不超过3个,后者最多不超过4个
6、程序一次运行生成的题目不能重复。
分析:判断题目是否重复,若重复,则不生成
7、在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。
分析:对生成的题目进行计算,结果存入指定文件
8、程序应能支持一万道题目的生成。
分析:程序生成一万道题目也不会出错
9、程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计。
分析:额外设置一个检查统计答案的模块,对输入的题目文件计算答案,与给定的答案文件比对,将统计结果存进指定文件
二、设计实现过程 :
1. 定义两个抽象类:
- Fraction类:真分数类,实现如何存放一个真分数,该类包含了实现加减乘除的计算与字符串的转换的函数;
- Question类:问题组成类,将问题拆分为编号,算术表达式,答案三个部分组成;
包含了返回答案的字符串形式和返回表达式的字符串形式的函数;
2. 六个相关的函数模块:
-
main模块:设置main函数通过命令行设置参数,函数接受到参数后首先判断参数个数是否符合要求;
然后根据"-n","-r","-e","-a"四个标志来获取需要的参数,再判断参数有无结对传入
以及传入的参数是否满足构成调用generateQuestions或check()函数的要求,若满足则调用
不满足要求则报错; -
GenerateQuestion模块:设置按数量与数值范围的要求生成题目集的函数generateQuestions(),定义
Question类集合获取生成的题目集,利用循环调用generateExpression()函数
来生成表达式,并调用evaluate()函数解析表达式生成答案,最后将序号,表达式、
答案包装成Question类对象添加进集合里,从而得到问题集,并调用; -
GenerateExpression模块:主要函数为生成表达式generateExpression()函数,该函数调用generateSimpleExpression()
生成简单表达式,调用generateComplexExpression()生成复杂表达式,且设置随机调用;而这两个函数
又会调用生成整数generateOperand()函数与生成真分数generateTrueFraction()函数来随机生成整数和真分数
以构造出随机算术表达式,最终通过generateExpression()函数返回算术表达式的字符串形式; -
ExpressionParsing模块:主要函数为evaluate()函数,通过设置操作数栈和操作符栈,对传进来的表达式进行一个个的读取入栈;
当字符为数字类型时入操作数栈,当字符为操作符类型时(通过isOperator()函数判断)入操作符栈;随后
通过判断操作符优先级(通过hasPrecedence()函数以及有无括号"()"来判断)计算出答案,该过程调用applyOp()
函数实现,且整个计算过程是一边入栈一边计算的,当满足计算要求时计算结果,然后将结果入栈,后续继续计算直到结束;
最后返回的是一个真分数类变量,该变量作为答案后续可进行自动字符串转换; -
CorrectExpression模块:设置check()函数获取两个文件路径参数,通过BufferedReader类读取文本内容,在函数中利用循环的单行读取来获取两个文件
相同序号题目的答案字符串,然后判断两个字符串大小是否相同,相同则代表答案一样,对correct字符串变量进行修改,不相同则
对wrong字符串变量进行修改,最终得到两个存放正确编号和错误编号的字符串变量,并将其打印进指定文件; -
Save模块:内含saveQuestionsToFile()函数通过循环来打印问题到问题文件中去,saveAnswerToFile()函数通过循环打印答案到答案文件;
三、 展示关键代码
- 用于生成算术表达式的GenerateExpression模块:
package main;
import java.util.Random;
public class GenerateExpression {
private static final String[] OPERATORS = {"+", "-", "×", "÷"};
private static final int MIN_OPERAND = 1; // 假设操作数的最小值为1
private static final int MIN_EXPRESSION_PARTS = 2; // 表达式至少包含两部分(两个操作数和一个操作符)
private static final Random random=new Random();
public static String generateExpression(int minParts, int maxParts,int MaxNumber) {
//随机生成算术表达式函数
if (minParts > maxParts || minParts < MIN_EXPRESSION_PARTS) {
throw new IllegalArgumentException("Invalid range for expression parts");
}
int parts = minParts + random.nextInt(maxParts - minParts + 1);
if (parts<3) {
return generateSimpleExpression(parts,MaxNumber);
} else {
return generateComplexExpression(parts,MaxNumber);
}
}
private static String generateSimpleExpression(int parts,int MaxNumber) {
//生成不带括号的简单算术表达式
StringBuilder sb = new StringBuilder();
if(parts==1){
if(random.nextBoolean()) sb.append(generateTrueFraction(MaxNumber));
else sb.append(generateOperand(MaxNumber));
sb.append(' ');
return sb.toString();
}
//int flat=0;减号的尝试处理
for (int i = 0; i < parts - 1; i++) {
if(random.nextInt(5)==1) sb.append(generateTrueFraction(MaxNumber));
else sb.append(generateOperand(MaxNumber));
sb.append(' ');
String Op;
Op=OPERATORS[random.nextInt(OPERATORS.length)];
//if(Op=="-")flat++;如果有减号,flat++
sb.append(Op);
sb.append(' ');
}
if(random.nextInt(5)==1) sb.append(generateTrueFraction(MaxNumber));
else sb.append(generateOperand(MaxNumber));
//尝试对减号进行处理,但没有达到相要的效果
/*if(flat!=0 ) {
String str = sb.toString();
String[] calculate = str.split("-");
Fraction f1 = ExpressionParsing.evaluate(calculate[0]);
Fraction f2 = ExpressionParsing.evaluate(calculate[1]);
Fraction f3 = f1.divide(f2);
if (f3.numerator < f3.denominator) return generateSimpleExpression(parts, MaxNumber);
}/* else if (calculate.length == 3) {
f1 = f1.subtract(f2);
f2 = ExpressionParsing.evaluate(calculate[2]);
f3 = f1.divide(f2);
if (f3.numerator < f3.denominator) return generateSimpleExpression(parts, MaxNumber);
}
}
/*else if(calculate.length==4){
f1=f1.subtract(f2);
f2=ExpressionParsing.evaluate(calculate[2]);
f3=f1.divide(f2);
if(f3.numerator<f3.denominator)return generateSimpleExpression(parts,MaxNumber);
else{
f1=f1.subtract(f2);
f2=ExpressionParsing.evaluate(calculate[3]);
f3=f1.divide(f2);
if(f3.numerator<f3.denominator)return generateSimpleExpression(parts,MaxNumber);
}
}
}*/
return sb.toString();
}
private static String generateComplexExpression(int parts, int MaxNumber) {
//生成带括号的复杂算术表达式
if (random.nextBoolean() ) {
return generateSimpleExpression(parts,MaxNumber);
}
int subParts = random.nextInt(parts-1)+1;
String left = generateSimpleExpression(subParts,MaxNumber);
String right = generateSimpleExpression(parts - subParts,MaxNumber);
if(subParts==1||parts-subParts==1)
return subParts==1?left+OPERATORS[random.nextInt(OPERATORS.length)] + "(" + right + ")":
"(" + left +")" + OPERATORS[random.nextInt(OPERATORS.length)] +right;
return "(" + left +")" + OPERATORS[random.nextInt(OPERATORS.length)] + "(" + right + ")";
}
//随机生成整数的函数
private static int generateOperand(int MAX_OPERAND) {
return MIN_OPERAND + random.nextInt(MAX_OPERAND - MIN_OPERAND + 1);
}
//随机生成真分数的函数
public static String generateTrueFraction(int MAX_OPERAND){
int denominator=random.nextInt(9)+1;//分母不等于0;
int numerator=random.nextInt(9)+1;
while (numerator >= denominator) {
numerator = random.nextInt(9) + 1;
denominator = random.nextInt(9) + 1;
}
int wholePart=random.nextInt(MAX_OPERAND);
if(wholePart==0)return numerator+"/"+denominator;
return wholePart+"'"+numerator+"/"+denominator;
}
}
- 用于计算答案的ExpressionParsing模块:
package main;
import java.util.Stack;
public class ExpressionParsing {
// 计算表达式的值
public static Fraction evaluate(String expression) {
Stack<Character> operators = new Stack<>();
Stack<Fraction> value = new Stack<>();
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (Character.isDigit(c)) {
// 处理多位数,例如"123"
int number = 0;
while (i < expression.length() && Character.isDigit(expression.charAt(i))) {
number = number * 10 + (expression.charAt(i) - '0');
i++;
}
int denominator=1;
int numerator=number;
//真分数处理,’后面是形如8/9的形式,共占据4个字符
if(i < expression.length() && expression.charAt(i)=='\''){
i++;
numerator = (expression.charAt(i++)-'0');
i++;
denominator = (expression.charAt(i++)-'0');
numerator+=number*denominator;
}
else if(i < expression.length() && expression.charAt(i)=='/'){
i++;
denominator= (expression.charAt(i++)-'0');
}
value.push(new Fraction(numerator,denominator));
i--; // 因为for循环也会增加i,所以需要减一以避免跳过字符
} else if(c==' '||c=='=') {
}//遇到空格符救跳过;
else if (c == '(') {
operators.push(c);
} else if (c == ')') {
// 计算括号内的表达式
while (!operators.isEmpty() && operators.peek() != '(') {
value.push(applyOp(operators.pop(), value.pop(), value.pop()));
}
// 弹出左括号
if (!operators.isEmpty()) {
operators.pop();
}
} else if (isOperator(c)) {
while (!operators.isEmpty() &&operators.peek()!='('&& hasPrecedence(c, operators.peek())) {
value.push(applyOp(operators.pop(), value.pop(), value.pop()));
}
operators.push(c);
}
}
// 处理剩余的运算符
while (!operators.isEmpty()) {
value.push(applyOp(operators.pop(), value.pop(), value.pop()));
}
return value.pop();
}
private static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '×' || c == '÷';
}
private static Fraction applyOp(char op, Fraction f1, Fraction f2) {
switch (op) {
case '+': return f1.add(f2);
case '-': return f2.subtract(f1);
case '×': return f1.multiply(f2);
case '÷': return f2.divide(f1); //
default:return new Fraction(1,1);
}
}
private static boolean hasPrecedence(char op1, char op2) {//op1的优先级更低或相等时返回true;
return (op1 != '×' && op1 != '÷') || (op2 != '+' && op2 != '-');
}
}
- 真分数类的定义Fraction模块
package main;
public class Fraction {
public int numerator; // 分子
public int denominator; // 分母
// 构造函数
public Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be zero");
}
//if (numerator < 0 || denominator < 0) {
//throw new IllegalArgumentException("Both numerator and denominator must be positive");
//}
this.numerator = numerator;
this.denominator = denominator;
reduce();
}
// 约分
private void reduce() {
int gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
}
// 计算最大公约数
private int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
// 加法
public Fraction add(Fraction other) {
int newNumerator = this.numerator * other.denominator + this.denominator * other.numerator;
int newDenominator = this.denominator * other.denominator;
return new Fraction(newNumerator, newDenominator);
}
// 减法
public Fraction subtract(Fraction other) {
int newNumerator = this.numerator * other.denominator - this.denominator * other.numerator;
int newDenominator = this.denominator * other.denominator;
return new Fraction(newNumerator, newDenominator);
}
// 乘法
public Fraction multiply(Fraction other) {
int newNumerator = this.numerator * other.numerator;
int newDenominator = this.denominator * other.denominator;
return new Fraction(newNumerator, newDenominator);
}
// 除法
public Fraction divide(Fraction other) {
if (other.numerator == 0) {
throw new IllegalArgumentException("Cannot divide by zero");
}
int newNumerator = this.numerator * other.denominator;
int newDenominator = this.denominator * other.numerator;
return new Fraction(newNumerator, newDenominator);
}
// 转换为字符串
@Override
public String toString() {
if(numerator==0)return numerator+"";
if(Math.abs(numerator)>=Math.abs(denominator)){
if(denominator==1)return numerator+"";
else if(denominator==-1)return -numerator+"";
else
{
int whole=numerator/denominator;
return whole+"'"+(numerator-whole*denominator)+"/"+denominator;
}
}
return numerator + "/" + denominator;
}
}
四、 测试代码模块:
- main模块:
package main;
import org.junit.jupiter.api.Test;
import java.io.IOException;
class mainTest {
@Test
public void TestMain() throws IOException {
//随机生成题目测试
String[] args={"-n10","-r10"};
main.main(args);
System.out.println("------------------");
//判断对错
String[] args1={"-eD:\\Exercises.txt","-aD:\\Answers.txt"};
main.main(args1);
System.out.println("------------------");
//全部正确传入
String[] args2={"-n10","-r10","-eD:\\Exercises.txt","-aD:\\Answers.txt"};
main.main(args2);
System.out.println("------------------");
//错误输入
String[] args3={"-n10"};
main.main(args3);
System.out.println("------------------");
//不成对出现正确参数
String[] args4={"-n10","-eD:\\Exercises.txt"};
main.main(args4);
System.out.println("-------------------");
//输入三个参数
String[] args5={"-r10","-n10","-eD:\\Exercises.txt"};
main.main(args5);
System.out.println("-------------------");
}
}
- GenerateQuestion模块:
package main;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class GenerateQuestionTest {
@Test
public void generateTest(){
int[] numberOfQuestions={10,100,1000,10000};
int[] maxNumber={1,10,100};
//10道题生成
List<Question> questions1=GenerateQuestion.generateQuestions(numberOfQuestions[0],maxNumber[0]);
System.out.println(questions1);
System.out.println("--------------");
//100道题生成
List<Question> questions2=GenerateQuestion.generateQuestions(numberOfQuestions[1],maxNumber[2]);
System.out.println(questions2);
System.out.println("--------------");
//1000道题
List<Question> questions3=GenerateQuestion.generateQuestions(numberOfQuestions[2],maxNumber[1]);
System.out.println(questions3);
System.out.println("--------------");
//10000道题生成
List<Question> questions4=GenerateQuestion.generateQuestions(numberOfQuestions[3],maxNumber[1]);
System.out.println(questions1);
System.out.println("--------------");
}
}
- ExpressionParsing模块:
package main;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ExpressionParsingTest {
@Test
public void evaluateTest(){
String[] args={"1+7=","(13+5)-8=","3×4+7'5/4=","3÷4+5'1/4"};
//简单加法测试
Fraction test1=ExpressionParsing.evaluate(args[0]);
System.out.println(test1.toString());
System.out.println("-------------------");
//复杂加减法测试
Fraction test2=ExpressionParsing.evaluate(args[1]);
System.out.println(test2.toString());
System.out.println("-------------------");
//引入真分数以及乘法测试
Fraction test3=ExpressionParsing.evaluate(args[2]);
System.out.println(test3.toString());
System.out.println("-------------------");
//引入真分数以及除法测试
Fraction test4=ExpressionParsing.evaluate(args[3]);
System.out.println(test4.toString());
System.out.println("-------------------");
}
}
五、测试报告:
-
代码覆盖率:
-
测试结果:main模块以及ExpressionParsing模块的测试正确运行,没有报错,但在GenerateQuestion模块测试时,由于没有
保证算数解析时计算过程出现分母不为0,所以在测试题目数量巨大时计算过程分母出现0的概率增大导致测试失败,所以不能正确运行;
-
部分题目以及答案生成的结果:
结果:
六、 效能分析:
- 由于未能及时导入Jprofile插件,暂时不能由软件实现自动分析;
- 个人意见程序主要耗时在ExpressionParsing()即表达式解析函数以及GenerateExpression()即生成表达式模块上,
且其中表达式解析函数由于开拓栈内存占用内存较多,另外GenerateQuestions()即生成相应要求的题目数量上占用内存也较多
七、代码存在的不足:
- 做出多种尝试但还是未能保证随机生成题目时的计算过程中出现除法的被除数是0,且仍有可能计算过程产生负数;
- 未能保证题目完全不重复的情况,虽然重复概率极低
八、总结
浩:
总结成败得失,分享经验教训
这次结对项目,我们基本上完成了题目的要求,主要不足之处可能是有些地方还需改进。不过我认为这次项目整体来说是成功的,
在完成过程中,我们学到了很多新知识,对java的掌握也更熟练,最重要的是,我们在解决一个难题时,进行了思想上火花的碰撞。
我们提出自己的观点,倾听理解对方的观点,表述自己的看法,共同思考解决方案,最终一个个问题迎刃而解。其中,我们获得了宝贵的经验,
比如说:针对一个模块的实现,先各自提供自己的思路,然后分析可行性,比较不同方法的效率;又或者说,在运行时,
通过逐一控制调用的模块,来检查程序问题所在;也获得了教训,比如说异常处理不够到位,导致程序打印乱码等等。
结对感受:
第一次与他人共同完成一个项目,觉得颇有成就感,并且在合作过程中思想的碰撞让我觉得受益匪浅。对方在一些问题上有着独特的见解,
而且对一些细节也能考虑到位,让我深感佩服,特别是能够仔细倾听和分析我提出的猜想。结对项目最重要的就是两个人及时的沟通和默契的配合,
经过这次项目,我对合作完成开发软件积累了宝贵的经验。
坤:
总结成败得失,分享经验教训
*优点:
二人合作提高了工作效率,减少了工作量;
相互监督督促,弥补了个人不足;
通过交流学习,提高了编程水平;
*缺点:
在合作完成项目时,偶尔会有理解分歧以及意见分歧从而消耗了一定时间
由于都是Java和git新手,在分工方面没有明确的准则,都是个人想到什么做什么,比较杂乱;
结对感受:
第一次与他人合作完成项目,过程虽跌宕起伏,但总体还是很不错的,特别享受与搭档沟通交流解决问题的过程,
对双方都受益良多,当成功完成程序的时候颇有成就感,编程过程出现有不足的地方,我们都一起不断的尝试过解决,
虽然有未能解决的地方,但总体来说还是满意的,总的来说这次合作我们都尽力了,贡献出了我们对这个项目所了解的一切,
相互弥补不足,扩大优点,受益匪浅。