结对项目
一、作业信息
| 这个作业属于哪个课程 | 班级链接 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 组队设计实现一个生成小学四则运算题目的命令行程序 |
二、小组成员
| 姓名 | 学号 | 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 List generateOperators(){
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());
}
}
八、项目小结
陈祖民:合作最重要的一点是沟通,在合作做项目时,我们需要合理的分配两个人需要完成的部分,偶尔双方会因为想法不同导致项目停工,需要不停更改项目,但从最终结果来看,我们还算是圆满的完成任务。总之,合作完成项目于我而言是一次非常好的经历,它让我明白项目永远不能只靠一个人的埋头苦干,还是需要大家一起集思广益,才能写出更大更好的项目。
陈大锴:本次程序的开发总体上比较成功,在完成了基本需求之后也完成了附加需求。在本次开发过程中,本人不仅收获了分模块开发的技巧,还收获了与他人合作互相交流想法的经验,虽然在某些区域也有些分歧,但是经过一番讨论后也达成了意见统一。我的搭档不仅提供了算法的思路,还协助进行了单元测试,找出了开发过程中的漏洞,希望下一次开发能继续合作。

浙公网安备 33010602011771号