结对项目
作业属于的课程 | 计科22级12班 |
---|---|
作业要求 | 要求 |
作业目标 | 团队合作实现一个自动生成小学四则运算题目的命令行程序 |
姓名 | 学号 |
---|---|
兰勇 | 3122004783 |
GitHub链接:链接
一、PSP表格
PSP2.1 | 描述 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | |||
· Estimate | 估计这个任务需要多少时间 | 60 | 60 |
Development | |||
· Analysis | 需求分析 (包括学习新技术) | 120 | 180 |
· Design Spec | 生成设计文档 | 60 | 60 |
· Design Review | 设计复审 | 30 | 30 |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | 具体设计 | 120 | 180 |
· Coding | 具体编码 | 480 | 600 |
· Code Review | 代码复审 | 60 | 30 |
· Test | 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | |||
· Test Report | 测试报告 | 120 | 90 |
· Size Measurement | 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1120 | 1360 |
二、效能分析
关于程序计算的部分,本来是希望通过将字符串转化为浮点型进行运算,但思考可以将所有的计算直接以字符串的形式进行,只是需要重新编写计算用的类。经过比较两者的性能后,最后决定使用自己编写计算用的Calculate类,比原来的性能有所提高。
三、设计实现过程
3.1项目结构
3.2具体实现
- Fraction类:自定义分数类,包含分数的分子分母,以及提供分数间加减乘除运算的函数。
点击查看代码
package lan;
import java.util.Random;
public class Fraction {
//分数的分子分母
public long fenZi;
public long fenMu;
private static Random random=new Random();
//随机构造分数
public Fraction(int range){
this.fenMu=random.nextInt(range-1)+1;
this.fenZi=random.nextInt(range);
simplify();
}
//由小数构造一个分数
/*public Fraction(double xiaoShu){
// 将小数转换为字符串以便处理
String xiaoShuString = String.valueOf(xiaoShu);
// 获取小数点的位置
int decimalPlaces = xiaoShuString.length() - xiaoShuString.indexOf('.') - 1;
// 计算分母
this.fenMu = (long)Math.pow(10, decimalPlaces);
// 计算分子
this.fenZi = (long) (xiaoShu * fenMu);
simplify();
}*/
//指定分子分母
public Fraction(long fenZi,long fenMu){
this.fenMu=fenMu;
this.fenZi=fenZi;
simplify();
}
// 分数加法
public static Fraction add(Fraction a,Fraction b) {
long newFenZi = a.fenZi * b.fenMu + b.fenZi * a.fenMu;
long newFenMu = a.fenMu * b.fenMu;
return new Fraction(newFenZi, newFenMu);
}
// 分数减法
public static Fraction sub(Fraction a,Fraction b) {
long newFenZi = a.fenZi * b.fenMu - b.fenZi * a.fenMu;
long newFenMu = a.fenMu * b.fenMu;
return new Fraction(newFenZi, newFenMu);
}
// 分数乘法
public static Fraction mul(Fraction a,Fraction b) {
long newFenZi = a.fenZi * b.fenZi;
long newFenMu = a.fenMu * b.fenMu;
return new Fraction(newFenZi, newFenMu);
}
// 分数除法
public static Fraction div(Fraction a,Fraction b) {
if (b.fenZi == 0) {
throw new ArithmeticException("Cannot divide by zero.");
}
long newFenZi = a.fenZi * b.fenMu;
long newFenMu = a.fenMu * b.fenZi;
return new Fraction(newFenZi, newFenMu);
}
// 计算最大公约数并简化分数
private void simplify() {
long gcd = Calculate.gcd(this.fenZi, fenMu);
this.fenZi /= gcd;
this.fenMu /= gcd;
// 确保分母为正
if (fenMu < 0) {
fenMu = -fenMu;
fenZi = -fenZi;
}
}
public static void main(String[] args){
Fraction f1=new Fraction(10);
System.out.println(Conversion.getFractionToString(f1));
Fraction f2=new Fraction(10);
System.out.println(Conversion.getFractionToString(f2));
System.out.println(Conversion.getFractionToString(add(f1,f2)));
System.out.println(Conversion.getFractionToString(sub(f1,f2)));
System.out.println(Conversion.getFractionToString(mul(f1,f2)));
System.out.println(Conversion.getFractionToString(div(f1,f2)));
}
}
-
Conversion类:方法类,提供将字符串转化为小数、分数以及将分数转化为字符串的函数。
-
Formula类:自定义公式类,提供合法公式的构造方法,实现获取公式答案以及以字符串形式获取公式。
-
Calculate类:计算方法类,实现计算一个以字符串输入的公式的答案。
-
IOUtil类:实现对文件的写入与读取。
-
MainUI类:提供要求要实现的两个功能,即随机创建一定数量的公式的函数和对比答案文件的函数。
-
Myapp类:主类,供用户使用。
点击查看代码
public class Myapp {
public static void main(String[] args){
if(args.length<4){
System.out.println("参数不足,请按如下格式输入:Myapp.exe -n <题目数量> -r <题目数值范围>");
return;
}
if(args[0].equals("-n")&&args[2].equals("-r")){
MainUI.creatFormula(Integer.parseInt(args[1]),Integer.parseInt(args[3]));
System.out.println("题目创建成功!题目文件为Exercise.txt,对应答案文件为Answer.txt。");
System.out.println("作答请按如下格式输入:Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt");
}
else if(args[0].equals("-e")&&args[2].equals("-a")){
MainUI.checkFormula(args[1],args[3]);
}
else {
System.out.println("输入参数有误,请按如下格式输入:Myapp.exe -n <题目数量> -r <题目数值范围> 或 Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt");
}
}
}
四、代码说明
4.1 Formula类构造公式。
定义一个公式类。具体构造方法为,先随机公式符号个数,根据符号个数随机生成数字以及符号,在将其以字符串的形式连接起来后,随机插入括号。然后执行对算式的检查(是否出现负数,公式之间是否重复等),如果检查不通过,则重复上述步骤至生成合法的公式。
点击查看代码
public class Formula {
private Random random=new Random();
//符号个数
private int symbolNum=random.nextInt(3)+1;
//算式符号数组
private String[] symbol=new String[symbolNum];
//算式数字数组
private String[] num=new String[symbolNum+1];
//算式字符串
private String formula;
//算式答案
private String answer;
//构造函数
public Formula(int range) {
do {
//生成随机符号
for (int i = 0; i < symbolNum; i++)
symbol[i] = getSymbol();
//生成随机数字
for (int i = 0; i < symbolNum + 1; i++) {
if (random.nextInt(2) == 0) {//生成自然数
num[i] = String.valueOf(random.nextInt(range));
} else {//生成分数
Fraction fraction = new Fraction(range);
num[i] = Conversion.getFractionToString(fraction);
}
}
//降低式子中存在负数的可能
for (int i = 0; i < symbolNum; i++)
if (symbol[i].equals(" - "))
if (Conversion.getStringToDigit(num[i]) < Conversion.getStringToDigit(num[i + 1])) {
//临时变量用于交换
String temp;
temp = num[i];
num[i] = num[i + 1];
num[i + 1] = temp;
}
//符号与数字组合得到算式
formula = num[0];
for (int i = 0; i < symbolNum; i++) {
formula = formula + symbol[i] + num[i + 1];
}
if (random.nextInt(2) == 0)//0则为算式插入括号
formula = insertKuoHao(formula);
formula=formula+" = ";
answer=Calculate.getFormulaAnswer(formula);
}while(answer.equals("error"));
}
public String getFormula(){return formula;}
public String getAnswer(){
return answer;
}
//随机插入括号
private String insertKuoHao(String str){
StringBuilder newExpression = new StringBuilder();
int start=random.nextInt(symbolNum);
int end=random.nextInt(symbolNum-start)+start+1;
for(int i=0;i<=symbolNum;i++){
if(i==start)
newExpression.append("(");
newExpression.append(num[i]);
if(i==end)
newExpression.append(")");
if(i<symbolNum)
newExpression.append(symbol[i]);
}
return newExpression.toString();
}
//给出随机符号
private String getSymbol(){
String[] symbol={" + "," - "," × "," ÷ "};
return symbol[random.nextInt(symbol.length)];
}
}
4.2 Calculate类实现公式计算。
公式以字符串的形式输入,再根据一定的运算规则得出答案以字符串返回。
点击查看代码
public class Calculate {
// 计算最大公约数
public static long gcd(long a, long b) {
while (b != 0) {
long temp = b;
b = a % b;
a = temp;
}
return a;
}
//计算算式
public static String getFormulaAnswer(String formula){
char[] tokens = formula.toCharArray();
Stack<String> values = new Stack<>(); // 存储操作数
Stack<Character> operators = new Stack<>(); // 存储操作符
String answer;
for (int i = 0; i < tokens.length; i++) {
// 当前字符是空格,跳过
if (tokens[i] == ' ') continue;
// 当前字符是数字
if (Character.isDigit(tokens[i])) {
StringBuilder sb = new StringBuilder();
while (i < tokens.length &&( Character.isDigit(tokens[i]) || tokens[i]=='\'' || tokens[i]=='/')) {
sb.append(tokens[i++]);
}
values.push(sb.toString());
i--; // 回退一个位置,继续处理
}
// 当前字符是左括号
else if (tokens[i] == '(') {
operators.push(tokens[i]);
}
// 当前字符是右括号
else if (tokens[i] == ')') {
while (!operators.isEmpty() && operators.peek() != '(') {
answer=applyOperator(operators.pop(), values.pop(), values.pop());
if(isRight(answer))
values.push(answer);
else
return "error";
}
operators.pop(); // 弹出左括号
}
// 当前字符是操作符
else if (isOperator(tokens[i])) {
while (!operators.isEmpty() && hasPrecedence(tokens[i], operators.peek())) {
answer=applyOperator(operators.pop(), values.pop(), values.pop());
if(isRight(answer))
values.push(answer);
else
return "error";
}
operators.push(tokens[i]);
}
}
// 处理剩余的操作符
while (!operators.isEmpty()) {
answer=applyOperator(operators.pop(), values.pop(), values.pop());
if(isRight(answer))
values.push(answer);
else
return "error";
}
return values.pop();
}
//运算法则
private static String applyOperator(char op, String stra,String strb) throws IllegalArgumentException, ArithmeticException {
Fraction a=Conversion.getStringToFraction(stra);
Fraction b=Conversion.getStringToFraction(strb);
switch (op) {
case '+':
return Conversion.getFractionToString(Fraction.add(a,b));
case '-':
return Conversion.getFractionToString(Fraction.sub(a,b));
case '×':
return Conversion.getFractionToString(Fraction.mul(a,b));
case '÷':
if(Conversion.getFractionToString(b).equals("0"))
return "error";
return Conversion.getFractionToString(Fraction.div(a,b));
default:
throw new IllegalArgumentException("Invalid operator: " + op);
}
}
//验证答案合理性
private static boolean isRight(String answer){
if(answer.equals("error"))
return false;
return !(Conversion.getStringToDigit(answer) <= 0);
}
//判断运算符
private static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '×' || c == '÷';
}
//决定运算符优先级
private static boolean hasPrecedence(char op1, char op2) {
if (op2 == '(' || op2 == ')') return false;
return (op1 != '×' && op1 != '÷') || (op2 != '+' && op2 != '-');
}
}
4.3 MainUI类实现具体命令。
creatFormula()通过重复调用Formula构造函数,创建Formula实例,然后通过Formula提供的方法将公式以字符串的形式和对应答案按照一定格式写入文件中。
checkAnswer()通过读取两个文件中的内容对比后得知答案的对错,并将其按一定格式写入另一个文件中。
点击查看代码
public class MainUI {
public static void creatFormula(int range,int amount){
IOUtil.clearFile("Exercises.txt");
IOUtil.clearFile("Answers.txt");
for(int i=0;i<amount;i++){
Formula formula=new Formula(range);
IOUtil.writeToFile("Exercises.txt",(i+1)+"."+formula.getFormula());
IOUtil.writeToFile("Answers.txt",(i+1)+"."+formula.getAnswer());
}
}
public static void checkFormula(String execrisePath,String answerPath){
ArrayList<String> correct=new ArrayList<>();
ArrayList<String> wrong=new ArrayList<>();
try (BufferedReader reader1 = new BufferedReader(new FileReader(answerPath));
BufferedReader reader2 = new BufferedReader(new FileReader("Answers.txt"))) {
String line1;
String line2;
while ((line1 = reader1.readLine()) != null&&(line2 = reader2.readLine()) != null) {
String[] parts1=line1.split("\\.");
String[] parts2=line2.split("\\.");
if(parts1[1].equals(parts2[1])){
correct.add(parts1[0]);
}else {
wrong.add(parts1[0]);
}
}
IOUtil.clearFile("Grade.txt");
IOUtil.writeToFileAppend("Grade.txt","Correct:"+correct.size()+"(");
int correctSize= correct.size();
for(int i=0;i<correctSize;i++) {
if(i!=correctSize-1)
IOUtil.writeToFileAppend("Grade.txt", correct.get(i)+",");
else{
IOUtil.writeToFile("Grade.txt", correct.get(i));
}
}
IOUtil.writeToFile("Grade.txt", ")");
IOUtil.writeToFileAppend("Grade.txt","Wrong:"+wrong.size()+"(");
int wrongSize= wrong.size();
for(int i=0;i<wrongSize;i++){
if(i!=wrongSize-1)
IOUtil.writeToFileAppend("Grade.txt", wrong.get(i)+",");
else{
IOUtil.writeToFileAppend("Grade.txt", wrong.get(i));
}
}
IOUtil.writeToFileAppend("Grade.txt", ")");
} catch (IOException e) {
e.printStackTrace();
System.out.println("未找到要检查的文件!");
}
}
}
五、测试运行
- 测试命令形式输入有误。
- 测试Fraction类中分数的加减乘除运算。
两个分数分别为1和7/9,对比加减乘除运算后发现答案正确。
- 测试Calculate类计算是否正确
测试代码
测试结果
-
测试MainUI类生成10000道题目
测试结果
-
测试MainUI类检测答案
测试例子
测试结果
六、项目小结
这一次项目虽然没有找到人能够结对,但也给我带来了不少收获,提高了我编写代码的能力,同时因为是一个人搞项目,需要考虑的更加全面,提高了我实现项目的能力。但同时这个项目也有点不足,没有完美实现作业要求,对于检查重复题目这个功能的实现,我只能考虑是将公式中的符号数组,数字数字,以及答案相互比较,考虑淘汰掉完全一致的公式,但这样所需要的时间应会明显增多,考虑到完成项目的时间较紧,故而没有能够实现。