一、作业信息
Github仓库地址:https://github.com/guzhouyiye/MathFormulaGenerator
这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 结对项目 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
姓名 | 学号 |
---|---|
陈国金 | 3122004301 |
廖俊龙 | 3118005817 |
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
·Estimate | ·估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1010 | 1370 |
·Analysis | ·需求分析(包括学习新技术) | 50 | 40 |
·Design Spec | ·生成设计文档 | 20 | 20 |
·Design Review | ·设计复审 | 10 | 5 |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 10 | 15 |
·Design | ·具体设计 | 30 | 40 |
·Coding | ·具体编码 | 850 | 1150 |
·Code Review | ·代码复审 | 30 | 60 |
·Test | ·测试(自我测试,修改代码,提交修改) | 10 | 40 |
Reporting | 报告 | 65 | 75 |
·Test Repor | ·测试报告 | 30 | 40 |
·Size Measurement | ·计算工作量 | 15 | 10 |
·Postmortem & Process Improvement Plan | ·事后总结,并提出过程改进计划 | 20 | 25 |
合计 | 1100 | 1475 |
三、模块接口的设计与实现过程
项目类与方法设计
1.MainMathFormulaGenerator类
用于接收命令行参数,调用其他工具类实现生成四则运算题目功能
2.CommandLineParser类
用于读取命令行参数,并对参数输入出现的问题进行输出。get方法用来得到所要的参数,has方法用来判断某个参数是否存在
3.EvaluateExpression类
用于计算运算式的类。其中expressionResult方法是主方法,其他类通过调用这个方法来获得计算结果。countExpression方法由expressionResult调用,根据提供的参数对某个范围的运算式进行计算。count方法由countExpression方法调用,用于两个数中间的加减乘除运算。commonDenominator方法用于给分数通分,由count调用,方便分数之间的运算。
4.existJudge类
负责判断运算式是否已经存在的类,通过对每两个式子进行compareNumberCount,compareResult,compareAllOperatorFix,compareAllNumberFix四个方法的调用比较来确定是否存在。
5.Expression类
负责生成运算式的类。Expression为该类的构造函数。generateNumber方法 随机生成自然数或真分数;simplify方法用于约分,gcd方法由simplify调用,来求最大公因数;generateOperator方法随机生成运算符;generateBracket随机生成括号。
6.ExpressionSet类
负责统筹所有运算式生成并写入文件的类。ExpressionSet构造函数由main方法调用,会通过调用其他工具类,根据参数生成对应数量的合法的运算式。并通过expressionsWrite方法和answersWrite方法将要写进文件的内容添加到字符串中,最后将字符串写入文件
7.JudgeExpressions类
负责判题的类。JudgeExpressions为构造函数,由main方法调用,通过调用其他工具类,判断并记录题目对错信息,最后写入文件。writeResult方法将将判题结果写入文件;rightJudge方法比较答案是否相同;separateExpression方法将运算式拆分成运算式、运算符、括号,然后调用EvaluateExpression类进行计算;getNumber将字符串里的运算数转化为Number类。
8.Number
用于创建对象,Number对象以分数形式保存运算数和答案,方便统一运算数的计算
流程图
四、效能分析
使用IDEA内置的IntelliJ Profile进行性能分析
1.热点图
2.方法列表
程序中消耗最大的函数
性能改进
消耗最大的方法是关于判断运算式是否出现过。改进时切换判断的顺序,使判断更高效。消耗时间从130ms减少到70ms
五、代码说明
ExpressionSet类,根据参数批量生成运算式,符合要求则添加,不符要求会排除重新生成。最后把运算式即答案写入文件。
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
// 负责统筹所有运算式生成并写入文件的类
public class ExpressionSet {
// 所有式子的列表
List<Expression> allExpression = new ArrayList<>();
// 创建文件
FileWriter expressionsTxt = new FileWriter("Exercises.txt");
FileWriter answersTxt = new FileWriter("Answers.txt");
StringBuilder expressionsString = new StringBuilder();
StringBuilder answersString = new StringBuilder();
public ExpressionSet(int questionCount, int range) throws IOException {
// 生成questionCount条式子
for (int i = 0; i < questionCount; i++) {
// 生成一条式子
Expression expression = new Expression(range);
// 如果式子结果不是负数且没有出现过
ExistJudge existJudge = new ExistJudge();
if (expression.result.denominator > 0 && !existJudge.existJudge(allExpression, expression)) {
allExpression.add(expression);
// 添加运算式到运算式字符串中
expressionsWrite(expressionsString, allExpression.get(i), i);
// 添加答案到答案字符串中
answersWrite(answersString, allExpression.get(i), i);
}
// 如果不合法
else {
i--;
}
}
expressionsTxt.write(expressionsString.toString());
answersTxt.write(answersString.toString());
expressionsTxt.close();
answersTxt.close();
}
// 运算式写入运算式字符串
private void expressionsWrite(StringBuilder expressionsString, Expression expression, int i) {
expressionsString.append(i + 1).append(". ");
// 同个位置判断顺序:'(' 运算数 运算符 ')'
// 左右括号判断(0左1右)
int judgeBracket = 0;
for (int j = 0; j < expression.numberCount; j++) {
// 添加左括号
if (expression.allBracket.size() == 2 && expression.allBracket.get(0) == j && judgeBracket == 0) {
expressionsString.append("(");
judgeBracket++;
}
// 添加运算数
// 自然数
if (expression.allNumberFix.get(j).denominator == 1 || expression.allNumberFix.get(j).numerator == 0) {
expressionsString.append(expression.allNumberFix.get(j).numerator);
}
// 分数
else {
// 判断真假分数
int judge = expression.allNumberFix.get(j).numerator / expression.allNumberFix.get(j).denominator;
// judge<1,真分数
if (judge < 1) {
expressionsString.append(expression.allNumberFix.get(j).numerator).append("/").append(expression.allNumberFix.get(j).denominator);
}
// judge>=1,假分数
else {
// 提取假分数的整数部分judge后,需要修改分数部分的分子
int fractionNumerator = expression.allNumberFix.get(j).numerator % expression.allNumberFix.get(j).denominator;
Number temp = expression.simplify(new Number(fractionNumerator, expression.allNumberFix.get(j).denominator));
expressionsString.append(judge).append("’").append(temp.numerator).append("/").append(temp.denominator);
}
}
// 添加右括号
if (expression.allBracket.size() == 2 && expression.allBracket.get(1) == j + 1 && judgeBracket == 1) {
judgeBracket++;
expressionsString.append(")");
}
// 添加运算符
if (j < expression.numberCount - 1) {
expressionsString.append(" ").append(expression.allOperatorFix.get(j)).append(" ");
}
}
expressionsString.append(" =" + "\n");
}
// 答案写入答案字符串
private void answersWrite(StringBuilder answersString, Expression expression, int i) {
answersString.append(i + 1).append(". ");
// 自然数
if (expression.result.denominator == 1) {
answersString.append(expression.result.numerator).append("\n");
}
// 分数
else {
// 判断真假分数
int judge = expression.result.numerator / expression.result.denominator;
// judge<1,真分数
if (judge < 1) {
answersString.append(expression.result.numerator).append("/").append(expression.result.denominator).append("\n");
}
// judge>=1,假分数
else {
// 提取假分数的整数部分judge后,需要修改分数部分的分子
int fractionNumerator = expression.result.numerator % expression.result.denominator;
answersString.append(judge).append("’").append(fractionNumerator).append("/").append(expression.result.denominator).append("\n");
}
}
}
}
Expression类,用于随机生成运算式中的运算数、运算符和括号位置,十分关键。
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
// 负责生成运算式的类
public class Expression {
private final Random random = new Random();
int numberCount = random.nextInt(3) + 2; // 运算式的数量(2,3,4)
List<Number> allNumber = new ArrayList<>(); // 运算式列表
List<Character> allOperator = new ArrayList<>(); // 运算符列表
List<Integer> allBracket = new ArrayList<>(); // 括号位置列表,位于相同索引运算数的左边
// 用来复制上面两个列表,作为固定列表,在判断运算式是否已经存在时使用
List<Number> allNumberFix = new ArrayList<>();
List<Character> allOperatorFix = new ArrayList<>();
// 用来存储运算结果
Number result = new Number();
public Expression(int range) {
// 生成数字、符号、括号列表
generateNumber(range);
generateOperator();
generateBracket();
EvaluateExpression evaluateExpression = new EvaluateExpression();
result = evaluateExpression.expressionResult(allNumber, allOperator, allBracket, 0, allOperator.size());
}
public Expression() {
}
// 随机生成自然数或真分数
private void generateNumber(int range) {
for (int i = 0; i < numberCount; i++) {
Number number = new Number();
boolean isFraction = random.nextBoolean();
// 生成分数(分母不为0)
if (isFraction) {
number.numerator = random.nextInt(range * (range - 1) -1) + 1;
//
int productNumberCount = 0;
do{
number.denominator = random.nextInt(range - 1) + 1;
if(productNumberCount++ == 10000000) System.out.println("参数r太小啦");
}while(number.numerator % number.denominator == 0 || number.numerator / number.denominator >= range);
// 调用simplify方法化简分数
number = simplify(number);
}
// 生成自然数
else {
number.numerator = random.nextInt(range - 1) + 1;
number.denominator = 1;
}
allNumber.add(number);
}
// 深拷贝 allNumber 到 allNumberFix
for (Number num : allNumber) {
// 这里为每个 num 创建一个新的 Number 对象,确保 allNumberFix 拷贝的是新的对象
allNumberFix.add(new Number(num.numerator, num.denominator));
}
}
// 约分
public Number simplify(Number number) {
int numerator = number.numerator;
int denominator = number.denominator;
int gcd = gcd(numerator, denominator);
number.numerator = numerator / gcd;
number.denominator = denominator / gcd;
return number;
}
// 求最大公因数
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 随机生成运算符
private void generateOperator(){
Character[] operator = new Character[]{'+', '-', '×', '÷'}; //运算符号
// 运算符数量是运算式数量减一
for (int i = 0; i < numberCount - 1; i++) {
allOperator.add(operator[random.nextInt(operator.length)]);
}
allOperatorFix = new ArrayList<>(allOperator);
}
// 随机生成括号
private void generateBracket(){
// 括号个数0或1
if(numberCount > 2 && random.nextBoolean()) {
int index = random.nextInt(numberCount - 1);
allBracket.add(index);
allBracket.add(index + 2);
}
}
}
六、测试说明
参数缺失
参数范围不正确
正确的输入输出
-n,-r参数
-e,-a参数
用标准答案,所以全部正确
使用自己故意修改前两道答案的MyAnswers.txt文件
生成一万道题目
七、项目小结
在这次四则运算自动生成项目中,我们通过分工合作,充分发挥各自的优势,实现了一个功能相对完整的工具。项目主要包括题目的生成、答案的计算与判断,以及结果的输出。
这次的主要收获有几个方面。一是在开发过程中,我们学会了如何有效地分工与协作。通过定期沟通,确保了各模块之间的顺利对接,提升了项目效率。二是在实现过程中,我们个人的技术得到,对项目开发有更深刻的理解。三是提升问题解决能力,项目中遇到的各种问题,锻炼了我们的调试和解决问题的能力。通过查阅资料和互相讨论,我们逐步克服了困难。
通过这个项目,我们不仅提升了技术能力,也增进了团队之间的默契,为今后更多的合作奠定了良好的基础。