结对项目
一、作业信息
这个作业属于哪个课程 | 班级链接 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 组队设计实现一个生成小学四则运算题目的命令行程序 |
二、小组成员
姓名 | 学号 | github |
---|---|---|
陈大锴 | 3122004816 | https://github.com/ez4-cdk/ez4-cdk/tree/master/3122004816/algorithm |
陈祖民 | 3122004822 | https://github.com/MIR-mIsTEo/3122004822-02 |
三、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 420 | 510 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 30 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 180 |
· Coding | · 具体编码 | 60 | 60 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 120 |
Reporting | 报告 | 120 | 180 |
· Test Repor | · 测试报告 | 60 | 120 |
· Size Measurement | · 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
· 合计 | 1200 | 570 | 720 |
四、设计实现过程
1、项目结构
2、接口设计与实现
①.Expression
此类为表达式的对象,成员为String类型的expression、构造方法Expression(String)、getter以及核心方法evaluate():String
②Fratcion
此类为分数的对象,对于分数的存储,选用"A'B/C"的带分数形式存储
加减乘除的思路为:
加法:分母分子同分,分子相加,分母取最小公倍数,返回的新分数再化简
减法:分母分子同分,分子相加,分母取最小公倍数,返回的新分数再化简
乘法:分子乘分子,分母乘分母
乘法:分子分母交叉相乘
成员主要有:
int 类型的num————整数部分
int 类型的numerator————分子部分
int 类型的denominator————分母部分
上面三个属性的getter,本分数的构造方法Fraction(Int,Int)
simplify() [将构造的分数化为带分数并且约分]
generateABiggerFraction():Fration [生成一个比本分数更大的分数]
add(Fratcion):Fraction [加]
subtract(Fratcion):Fraction [减]
multiply(Fratcion):Fraction [乘]
divide(Fratcion):Fraction [除]
fromString(String):Fraction [将表达式转化为分数]
gcd(int,int):int [求最大公约数]
toString():String [重写的tostring()方法]
③ExpressionEvaluator
此类为表达式计算器,用于计算表达式的值,先计算括号内的,再计算乘除,最后计算加减
evaluate(String):Fraction 计算一个表达式并返回一个分数
calculate(String):String 识别括号并进入括号内计算的过程
evaluateExpression(String):Fraction 计算的主体过程,包括加减乘除
④FileIO
此类为文件输入输出的工具类
readFile(String):String [读入]
writeOutput(String,Object):String [写出]
⑤Generator
此类为生成器工具类,用户生成各种需要的数据,属于关键类,轴心流程图如下:
开始 | v generateExpressions(num, area) —— > 生成表达式集合的核心函数,num为表达式数量,area为表达式各个参数的取值范围 | v 循环 i 从 0 到 num-1 —— > 生成每一条表达式 | v generateOperators() —— > 生成表达式的操作符 | v generateNumber(operators.size() + 1, area, operators) —— > 生成表达式的操作数 | v generateExpression(operators, numbers) —— > 表达式的操作符与操作数按照需求组合 | v 添加表达式到 expressions 列表 | v 结束循环 | v 返回 expressions 列表 | v 结束
Random 类型的random————随机种子
generateNumber(int,int,List
generateOperators():List
sort(List
generateExpressions(int,int):List
generateBracket(int):List
generateExpression(List
getOperatorSymbol(int):String [将加减乘除符号解析]
⑥SplashClass
此类为入口类,入口有两个,一个是生成题目与答案的入口,一个是检查答案与表达式答案的入口
main(String[]):void [main函数]
generateExercises(Integer,Integer):void [生成题目的入口]
checkAnswer(String,String):void [检查答案的入口]
五、代码说明
Generator
生成器的类是本程序的核心,负责控制生成操作符、操作数、表达式、表达式集合,层层递进
package com.softwareClass.util; import com.softwareClass.entity.Expression; import com.softwareClass.entity.Fraction; import java.util.*; public class Generator { public ListgenerateOperators(){ int randomArea = 3; List list = new ArrayList<>(); int operatorsCount = random.nextInt(3)+1; for(int i=0; i generateNumber(int count, int area, List operators) { List numbers = new ArrayList<>(); try { //循环生成数量为count的分数集合 while (numbers.size() < count) { int denominator = random.nextInt(area-3) + 3; int numerator = random.nextInt((area - 1) * denominator) + 1; Fraction fraction = new Fraction(numerator, denominator); numbers.add(fraction); } //排序 sort(numbers); //对于除法,选择生成一个比前一个更大的分数 for (int i = 0; i < operators.size(); i++) { if (operators.get(i) == 3) { numbers.set(i+1,numbers.get(i).generateABiggerFraction()); } } } catch (IllegalArgumentException e) { throw new IllegalArgumentException("随机种子初始化失败,请重试."); } return numbers; } //排序(降序) private void sort(List numbers) { numbers.sort((f1, f2) -> { int value1 = f1.getNum(); int value2 = f2.getNum(); return Integer.compare(value2, value1); }); } //主生成函数——生成表达式集合 public List generateExpressions(int num,int area) { List expressions = new ArrayList<>(); while (expressions.size() < num){ List operators = generateOperators(); List numbers = generateNumber(operators.size()+1, area,operators); Expression e = new Expression(generateExpression(operators,numbers)); if (Fraction.fromString(e.evaluate()).getNum()>=0){ expressions.add(e); } } return expressions; } //生成括号——在特定索引下生成括号,如果索引合法,则返回索引集合,非法则不生成括号 public List generateBracket(int num){ List brackets = new ArrayList<>(); int leftBracket = random.nextInt(num); int rightBracket = random.nextInt(num); if (leftBracket operators, List numbers) { List numberList = new ArrayList<>(numbers); List brackets = generateBracket(numberList.size()); StringBuilder expressionBuilder = new StringBuilder(); //类似于e=()这种情况就不用加括号,比如e=(1+2/3) if (brackets!=null&&brackets.get(0)==0&&brackets.get(1)==numberList.size()-1){ brackets = null; } //每次加入一个分数和一个符号 for (int i = 0; i < operators.size(); i++) { //左括号索引 if (brackets!=null&&brackets.get(0)==i){ expressionBuilder.append("("); } //右括号索引 expressionBuilder.append(numberList.get(i)); if (brackets!=null&&brackets.get(1)==i){ expressionBuilder.append(")"); } expressionBuilder.append(getOperatorSymbol(operators.get(i))); } //添加最后一个分数 expressionBuilder.append(numberList.getLast()); //如果右括号没合上,则合上 if (brackets!=null&&(brackets.get(1)+1)==numberList.size()){ expressionBuilder.append(")"); } return expressionBuilder.toString(); } // 辅助方法,用于根据操作符的整数值返回相应的符号 public String getOperatorSymbol(int operator) { return switch (operator) { case 0 -> " + "; case 1 -> " - "; case 2 -> " * "; case 3 -> " / "; default -> throw new IllegalArgumentException("Invalid operator"); }; } }
Fraction
分数的类,至关重要,对每个分数的加减乘除和解析负责
package com.softwareClass.entity; public class Fraction { private int num; // 整数部分 private int numerator; // 分子 private int denominator; // 分母 //三个属性的getter public int getNum() { return num; } public int getNumerator() { return numerator; } public int getDenominator() { return denominator; } //构造函数 public Fraction(int numerator, int denominator) { if (denominator == 0) throw new IllegalArgumentException("分母不能为零"); this.numerator = numerator; this.denominator = denominator; simplify(); // 在构造时简化分数 } //生成一个比本分数还大的分数 public Fraction generateABiggerFraction(){ if (this.num==0&&this.numerator==0)return new Fraction(1,1); int thisNumerator = this.num*this.denominator+this.numerator; int thisDenominator = this.denominator; if (thisDenominator/2 == 0){ thisDenominator=3; }else { thisDenominator=thisDenominator/2; } return new Fraction(thisNumerator,thisDenominator); } //从字符串解析出分数 public static Fraction fromString(String str) { int denominator,numerator,num; String[] temp1,temp2; if (str.contains("/")){ if (str.contains("'")){ temp1 = str.split("'"); num = Integer.parseInt(temp1[0]); temp2 = temp1[1].split("/"); denominator = Integer.parseInt(temp2[1]); numerator = Integer.parseInt(temp2[0]); return new Fraction(num*denominator+numerator,denominator); }else{ temp1 = str.split("/"); denominator = Integer.parseInt(temp1[1]); numerator = Integer.parseInt(temp1[0]); return new Fraction(numerator,denominator); } }else{ return new Fraction(Integer.parseInt(str),1); } } //加法,分母相乘,分子交叉乘分母并相加 public Fraction add(Fraction other) { int commonDenominator = this.denominator * other.denominator; int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator + (other.numerator + other.num * other.denominator) * this.denominator; return new Fraction(newNumerator, commonDenominator); } //减法,分母相乘,分子交叉乘分母并相减 public Fraction subtract(Fraction other) { int commonDenominator = this.denominator * other.denominator; int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator - (other.numerator + other.num * other.denominator) * this.denominator; return new Fraction(newNumerator, commonDenominator); } //乘法,分子分母对应相乘 public Fraction multiply(Fraction other) { int newNumerator = (this.numerator + this.num * this.denominator) * (other.numerator + other.num * other.denominator); int newDenominator = this.denominator * other.denominator; return new Fraction(newNumerator, newDenominator); } //除法,分子分母交叉相乘 public Fraction divide(Fraction other) { if (other.numerator == 0 && other.num == 0) throw new IllegalArgumentException("不能除以零"); int newNumerator = (this.numerator + this.num * this.denominator) * other.denominator; int newDenominator = this.denominator * (other.numerator + other.num * other.denominator); return new Fraction(newNumerator, newDenominator); } //化简分数 private void simplify() { int gcd = gcd(Math.abs(numerator), Math.abs(denominator)); // 计算最大公约数 // 更新整数部分与分子、分母 this.num = this.numerator/this.denominator; this.numerator = this.numerator % this.denominator; this.numerator /= gcd; this.denominator /= gcd; // 确保分母为正 if (denominator < 0) { numerator = -numerator; denominator = -denominator; } } //求最大公约数 private int gcd(int a, int b) { while (b != 0) { int temp = b; b = a % b; a = temp; } return Math.abs(a); // 返回绝对值以确保结果为正 } @Override public String toString() { if (num == 0) { if (numerator == 0) { return "0"; // 处理0的情况 } else { return numerator + "/" + denominator; // 只返回分数 } } else { if (numerator == 0) { return String.valueOf(num); // 只返回整数部分 } return num + "'" + numerator + "/" + denominator; // 返回带分数 } } }
六、测试运行
测试代码
import com.softwareClass.SplashClass; import org.junit.jupiter.api.Test; import java.io.IOException; public class SplashClassTest { @Test public void testGenerateExercises(){ try { SplashClass.main(new String[]{"Myapp.exe","-n","10","-r","10"}); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void testCheckAnswer(){ try { SplashClass.main(new String[]{"Myapp.exe","-e","Exercises.txt","-a","Answer.txt"}); } catch (IOException e) { throw new RuntimeException(e); } } }
testGenerateExercises()测试结果
在当前目录下生成了两个文件Exercises.txt和Answer.txt
使用标准答案测试testCheckAnswer()
在生成的Grade.txt文档里全部都是正确的,经过数学验算也可得知答案没有出错
修改Answer.txt:答案(1,3,5,7)分别加1
使用修改后的答案测试testCheckAnswer()
(为了更好地对比,不加清除文本原来内容的处理逻辑)
生成一万条题目
修改测试代码
检查生成结果
七、效能分析
Jprofile 数据
Idea内置的IntelliJ Profile数据
可以看到耗时最大的函数为文件输入输出的函数FileIO
一次一次地写入文件是一件很耗时的操作,所以我们改进了一下写入操作:
private static void generateExercises(Integer num,Integer area) throws IOException { int index=0; try { String EXERCISES_FILE = "Exercises.txt"; String ANSWERS_FILE = "Answer.txt"; // 使用 StringBuilder 进行批量写入 StringBuilder exerciseContent = new StringBuilder(); StringBuilder answerContent = new StringBuilder(); for (Expression e : new Generator().generateExpressions(num, area)) { index++; exerciseContent.append((index + 1)) .append(". ") .append(e.getExpression()) .append(System.lineSeparator()); answerContent.append((index + 1)) .append(". ") .append(e.evaluate()) .append(System.lineSeparator()); } // 一次性写入文件 FileIO.writeOutput(EXERCISES_FILE, exerciseContent.toString()); FileIO.writeOutput(ANSWERS_FILE, answerContent.toString()); } catch (IOException exception) { throw new IOException(exception.getMessage()); } }
八、项目小结
陈祖民:合作最重要的一点是沟通,在合作做项目时,我们需要合理的分配两个人需要完成的部分,偶尔双方会因为想法不同导致项目停工,需要不停更改项目,但从最终结果来看,我们还算是圆满的完成任务。总之,合作完成项目于我而言是一次非常好的经历,它让我明白项目永远不能只靠一个人的埋头苦干,还是需要大家一起集思广益,才能写出更大更好的项目。
陈大锴:本次程序的开发总体上比较成功,在完成了基本需求之后也完成了附加需求。在本次开发过程中,本人不仅收获了分模块开发的技巧,还收获了与他人合作互相交流想法的经验,虽然在某些区域也有些分歧,但是经过一番讨论后也达成了意见统一。我的搭档不仅提供了算法的思路,还协助进行了单元测试,找出了开发过程中的漏洞,希望下一次开发能继续合作。