结对项目
概况
| 完成人员 | 学号 |
|---|---|
| 钟京洲 | 3121005063 |
| 唐梦思 | 3221005284 |
作业概述
| 软件工程 | 计科21级3班 + 4班 |
|---|---|
| 作业要求 | 四则运算生成 |
| Github地址 | 仓库 |
PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
| Development | 开发 | 1755 | 2145 |
| · Analysis | · 需求分析 (包括学习新技术) | 130 | 180 |
| · Design Spec | · 生成设计文档 | 60 | 35 |
| · Design Review | · 设计复审 (和同事审核设计文档) | 5 | 5 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
| · Design | · 具体设计 | 200 | 120 |
| · Coding | · 具体编码 | 1200 | 1500 |
| · Code Review | · 代码复审 | 30 | 120 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 |
| Reporting | 报告 | 85 | 130 |
| · Test Report | · 测试报告 | 60 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | 90 |
| 合计 | 1870 | 2305 |
项目
需求概况
| 需求描述 | 是否实现 |
|---|---|
| 控制生成题目的个数 | 是 |
| 控制题目中数值范围 | 是 |
| 计算过程不能产生负数,除法的结果必须是真分数,题目不能重复,运算符不能超过3个 | 是 |
| 生成的题目存入执行程序的当前目录下的Exercises.txt文件 | 是 |
| 题目的答案存入执行程序的当前目录下的Answers.txt文件 | 是 |
| 能支持一万道题目的生成 | 是 |
| 支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计 | 是 |
| 统计结果输出到文件Grade.txt | 是 |
需求分析
- 使用命令行操作,项目打包方式为jar包。
- 文件读写操作。
- 四则运算表达式生成、中缀表达式到后缀的转化。
- 去重
项目设计
项目概况

计算与生成表达式
Fraction
Fraction类为存储计算数的基本类,包含用辗转相除法求最大公约数,化假分数为带分数的方法,代码如下
点击查看代码
/**
* @author zjz
* @date 2023/9/23
*/
@Data
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class Fraction {
/**
* 分母默认为1
*/
private int denominator;
/**
* 分子
*/
private int numerator;
/**
* 用辗转相除法求最大公约数
* @param a 被除数
* @param b 除数
* @return 最大公约数
*/
private static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
/**
* 对分数进行约分
*/
public void Appointment() {
if (numerator == 0 || denominator == 1)
return;
// 如果分子是0或分母是1就不用约分了
int gcd = gcd(numerator, denominator);
this.numerator /= gcd;
this.denominator /= gcd;
}
public String transferFraction(){//对分数进行约分化简
if (this.denominator == 0) this.denominator = 1;
int c = this.numerator/gcd(this.numerator,this.denominator);
int d = this.denominator/gcd(this.numerator,this.denominator);
int e = c/d;
int f = c%d;
String str = "";
if(f==0){
str += e;
}else if(e!=0){
str = e+"'"+f+"/"+d;
}else{
str +=f+"/"+d ;
}
return str;
}
@Override
public String toString() {
return numerator + "/" + denominator;
}
}
FractionUtil
包含加减乘除等一系列对Fraction的操作,以及将带分数转化为Fraction对象的方法
点击查看代码
public class FractionUtil {
/**
* 加法
*/
public static Fraction add(Fraction f1, Fraction f2) {
return new Fraction(f2.getDenominator() * f1.getDenominator(),f2.getNumerator() * f1.getDenominator() + f2.getDenominator() * f1.getNumerator());
}
/**
* 减法
*/
public static Fraction sub(Fraction f1, Fraction f2) {
return new Fraction(f2.getDenominator() * f1.getDenominator(), f2.getDenominator() * f1.getNumerator() - f2.getNumerator() * f1.getDenominator());
}
/**
* 分数的乘法运算
*/
public static Fraction multiplication(Fraction f1, Fraction f2) { // 乘法运算
return new Fraction(f2.getDenominator() * f1.getDenominator(),f2.getNumerator() * f1.getNumerator());
}
/**
* 分数除法运算
*/
public static Fraction div(Fraction f1, Fraction f2) {
return new Fraction(f2.getNumerator() * f1.getDenominator(),f2.getDenominator() * f1.getNumerator());
}
/**
* 带分数转化为Fraction对象
* @param properFraction 带分数
* @return Fraction
*/
public static Fraction Transform(String properFraction,boolean type){
if (type) {
return new Fraction(1,Integer.parseInt(properFraction));
} else {
String[] split = properFraction.split("'");
int denominator;
int numerator;
if (split.length == 1) {
// 普通分数
split = properFraction.split("/");
denominator = Integer.parseInt(split[1]);
numerator = Integer.parseInt(split[0]);
} else {
String[] split1 = split[1].split("/");
denominator = Integer.parseInt(split1[1]);
numerator = Integer.parseInt(split[0]) * denominator + Integer.parseInt(split1[0]);
}
Fraction fraction = new Fraction();
fraction.setDenominator(denominator);
fraction.setNumerator(numerator);
return fraction;
}
}
}
CalculateImpl
- 中缀转后缀时,采用一个队列和一个栈,数字输入到队列,运算符压入栈。在压入栈时,检查栈顶元素的优先级,要保证压入栈的元素的优先级最高。若栈顶元素大于等于想压入栈的元素,则把栈顶元素弹出,继续检查,直到被压入栈的元素的优先级是最高的。遇到"(“先入栈,遇到”)“把”("之后的元素弹出到队列,括号不输出到队列。
- 后缀表达式求值时,遇到数字就压入栈,遇到运算符就弹出两个数字进行运算。因为最后的结果是分数,所以从一开始运算就把运算数全部转换为分数进行计算最后化简。
点击查看代码
public class CalculateImpl implements Calculate{
@Override
public Fraction fourKindCalculate(Fraction f1,Fraction f2, String operator) {
Fraction result;
switch (operator) {
case "+":
result = FractionUtil.add(f1,f2);
break;
case "-":
result = FractionUtil.sub(f1,f2);
break;
case "*":
result = FractionUtil.multiplication(f1,f2);
break;
case "/":
// 除数不为0则返回结果,除数为0则报错
if (f2.getDenominator() != 0) {
result = FractionUtil.div(f1,f2);
} else {
throw new DivisionWrongException();
}
break;
default:
throw new WrongOperationException();
}
return result;
}
@Override
public String calculate(String str) {
String[] compute = str.split(" ");
//存后缀表达式
Stack<String> stack = new Stack<>();
//放操作符
Stack<String> optStack = new Stack<>();
//放数字结果
Stack<Fraction> numStack = new Stack<>();
for (String s : compute) {
// 带分数
if (s.matches("^(\\d+)'(\\d+)\\/(\\d+)$")) {
// 直接压入栈中,作为一个数来计算
stack.push(s);
}
//匹配数字
else if (s.matches("-?\\d+(\\.\\d+)?")) {
stack.push(s);
} else if (optStack.isEmpty()) {
optStack.push(s);
}
//判断符号
else if ("(".equals(s)) {
optStack.push(s);
} else if (")".equals(s)) {
while (!"(".equals(optStack.peek())) {
stack.push(optStack.pop());
}
//找到(
optStack.pop();
}
//加减乘除入栈
else if ("+".equals(s) || "-".equals(s) || "*".equals(s)
|| "/".equals(s)) {
while ((!optStack.isEmpty()) && (PriorityTable.Priority(s) <= PriorityTable.Priority(optStack.peek()))) {
stack.push(optStack.pop());
}
optStack.push(s);
}
//数字小数点直接入栈
else {
stack.push(s);
}
}
//当操作符栈不为空,将操作符全部弹入stack
while (!optStack.isEmpty()) {
stack.push(optStack.pop());
}
//numStack=
Stack<Fraction> traversing = Traversing(stack, numStack);
return traversing.pop().transferFraction();
}
@Override
public Stack<Fraction> Traversing(Stack<String> stack,Stack<Fraction> numStack){
for (String l : stack) {
Fraction num1, num2;
//正则表达式匹配分数
if (l.matches("^(\\d+)(?:'(\\d+))?/(\\d+)$")){
numStack.push(FractionUtil.Transform(l,false));
} else if (l.matches("^\\d+$")){
numStack.push(FractionUtil.Transform(l,true));
}
else {
if(numStack.isEmpty()){
num1 = new Fraction(1,0);
}
else {
num1 = numStack.pop();
}
if(numStack.isEmpty()){
num2 = new Fraction(1,0);
}
else {
num2 = numStack.pop();
}
Fraction calculate = fourKindCalculate(num2,num1,l);
numStack.push(calculate);
}
}
return numStack;
}
}
ExpressionImpl
- List
createExpression(Integer n, Integer r)
运算数和运算符是随机生成的,每一次循环生成一个运算数和一个运算符,limit控制循环的次数,即运算符的数目,在这个方法中对不合法和重复的式子进行剔除,不合法的情况有分母为零,出现负数。在调用Calculate方法计算的时候,把含有以上两种情况的式子都会剔除。
点击查看代码
public class ExpressionImpl implements Expression {
/**
* 生成的表达式集合
*/
ArrayList<String> expressionList = new ArrayList<>();
/**
* 写入文件的表达式的集合
*/
ArrayList<String> expressionListToFile = new ArrayList<>();
@Override
public List<String> createExpression(Integer n, Integer r) {
String[] operate = new String[]{"+", "-", "*", "/"};
Random random = new Random();
//随机产生的运算符个数
int count;
StringBuilder temp;
String tempToFile;
for (int i = 0; i < n; i++) {
//[1,4)
temp = new StringBuilder();
count = random.nextInt(3) + 1;
for (int j = 1; j <= count; j++) {
Fraction fraction = new Fraction(random.nextInt(n), random.nextInt(n));
String opt = operate[random.nextInt(4)];
// 这里要判断是否分母可能为0,如果为0则按1来计算,分数类分母默认为1
temp.append(fraction.transferFraction()).append(" ").append(opt).append(" ");
}
int x2 = random.nextInt(n);
temp.append(x2);
tempToFile = i+1+"、"+temp + "=";
expressionList.add(temp.toString());
expressionListToFile.add(tempToFile);
}
return expressionList;
}
@Override
public void writeToFile(String fileName) {
FileUtil.writeToFile(expressionListToFile,fileName);
}
@Override
public void answerToFile(List<String> answerList, String fileName) {
FileUtil.writeToFile(answerList,fileName);
}
}
表达式结果校验与输入输出
Check
其对应的实现类CheckImpl,使用FileUtil将答题情况写入Grade文件,调用Calculate计算对应结果并且校验,使用linkedHashMap存放题目和答案。
点击查看代码
/**
* 读取题目和答案文件
*
* @param answerFile 答案文件
* @return exercises集合
*/
Map<String,String> readFile(File answerFile) throws Exception;
/**
* 对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
*
*/
Result checkAnswer(Map<String,String> map) throws IOException;
/**
* 对给定的题目文件和答案文件,判定答案中的对错并进行数量统计
*/
Result checkPaper(String answerFile);
测试
生成表达式
public void testCreateExpression() {
List<String> expression = this.expression.createExpression(5, 5);
for (String s : expression) {
System.out.println(s);
}
}

存入txt文本

问题:

答案:

一万道题目生成及其性能分析
Expression expression = new ExpressionImpl();
Calculate calculate = new CalculateImpl();
String answerTemp;
List<String> expression1 = expression.createExpression(10000, 10);
List<String> answerList = new ArrayList<>();
expression.writeToFile("Exercises.txt");
for (String s : expression1) {
String result = calculate.calculate(s);
answerTemp = s + " =" + result;
System.out.println(answerTemp);
answerList.add(answerTemp);
}
expression.answerToFile(answerList, "Answers.txt");
}




答案校验
题目:

答案:

其中修改了前三题的答案,所以grade输出应该是前三题错误,其余正确
校验:

七、总结
收获:
1 锻炼了团队协作和沟通的能力,学会倾听和尊重队友的意见,并在最终的决策过程达成共识。
2 为完成项目需求,需要分析具体的实现方式来解决遇到的难题,学习新的思维模式,采用相关技术克服难题,在这个过程锻炼了逻辑思维,提升了编程能力
3 结对编程体现了合作的关键性,在项目经过需求分析和详细设计后,要进入真正实现的过程,对于程序的推进,需要明确各自的目标,尽可能达到最优的实现方案,完成负责的模块,提高开发效率
不足的地方:
用Java来实现数据结构的效率不够高,没有图形化界面

浙公网安备 33010602011771号