这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230 |
这个作业的目标 | <结对完成实现一个自动生成小学四则运算题目的命令行程序> |
项目成员 | 李响 3121002802 |
项目成员 | 欧可贵3121002717 |
一、Github链接
https://github.com/Placidoe/homework.git
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 40 |
Estimate | 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 150 | 200 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
Design Spec | 生成设计文档 | 20 | 20 |
Design Review | 设计复审 | 10 | 10 |
Coding Standard | 需求分析 (包括学习新技术) | 30 | 20 |
Design | 具体设计 | 60 | 60 |
Coding | · 具体编码 | 180 | 150 |
· Code Review | · 代码复审 | 20 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 50 |
Reporting | 报告 | 30 | 30 |
· Test Repor | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 10 |
· 合计 | 650 | 780 |
三、需求
使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
每道题目中出现的运算符个数不超过3个。
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
四则运算题目1
四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
答案1
答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
程序应能支持一万道题目的生成。
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
四、模块接口的设计与实现过程
两个类(DataStore、IOUtils)
DataStore 类是一个单例类,使用了双重检查锁定的方式来保证线程安全,并且提供了多种方法来生成和计算数学表达式。
public void insertOp():
随机选择一个操作符(+、-、*、/)并将其存入 tokens 数组。
public void insertNum():
随机生成一个操作数(范围为 0 到 num2)并将其转换为字符串形式存入 tokens 数组。
public void InitData(String[] args):
根据传入的参数初始化 op1、num1、op2 和 num2。如果参数数量不符合预期,则根据默认值进行初始化。
public void InitTokens():
初始化 tokens 数组并生成随机的操作数和操作符。
public int evalRPN(String[] tokens):
接收一个字符串数组(反向波兰表示法),使用栈计算结果。对于每个操作符,弹出相应的操作数进行计算,并将结果压回栈中。使用 StringBuilder 构建表达式的字符串。
public boolean isNumber(String token):
判断传入的字符串是否为数字。
public void Run():
运行生成表达式的主方法,循环指定次数生成数学表达式、计算结果,并校验表达式的唯一性。
public int caculate(String op,int num1,int num2):
根据传入的操作符和两个操作数计算并返回结果,处理基本的加、减、乘、除运算。
static public DataStore getInstance():
单例模式实现,确保在多线程环境中只创建一个 DataStore 实例。
IOUtils类
public static void saveExercises(String exercises):
参数:接收一个字符串参数 exercises,表示要保存的题目内容。
文件写入:
使用 BufferedWriter 包裹 FileWriter,使得写入操作更高效。
new FileWriter("Exercises.txt", true):以追加模式打开文件,如果文件不存在则会创建它。
writer.write(exercises):将题目写入文件。
writer.newLine():在写入的内容后添加换行符。
异常处理:
使用 try-with-resources 语句确保 BufferedWriter 在使用后自动关闭。
捕获 IOException 异常,并通过 e.printStackTrace() 打印异常信息。
public static void saveAnswers(String answers):
参数:接收一个字符串参数 answers,表示要保存的答案内容。
文件写入:采用相同的方式打开 Answers.txt 文件并写入内容。
异常处理:同样使用 try-with-resources 和 IOException 捕获,确保代码的安全性。
五、代码说明
DataStore.java文件:
整个 DataStore 类的设计思路是:
表达式生成: 随机生成一个数学表达式,确保操作数和操作符的随机性。
逆波兰表达式计算: 实现了一个计算逆波兰表达式的算法,能有效地处理各种操作符。
唯一性校验: 使用集合来确保生成的数学表达式是唯一的,避免重复。
这段代码的实现思路在于结合数学运算与数据结构(栈)来高效地生成和计算表达式,并通过策略来确保生成表达式的有效性和唯一性。
源代码:
点击查看代码
package com.lx.single;
import com.lx.utils.FractionGenerator;
import com.lx.utils.IOUtils;
import lombok.Data;
import java.util.*;
/**
* TODO
*
* @Description
* @Author Lx
* @Date 2024/9/26 上午10:46
**/
@Data
public class DataStore {
private static volatile DataStore dataStore;
/*
策略1:
1.从栈顶弹出a1-操作数
2.
从栈顶弹出a2-操作符(如果是+或-)则继续下一步单暂不运算,而是判断后面一个是否为(*或/),依次循环往后看,直到遇到(+或-)。
如果是(*或/)继续下一步就直接进行运算
3.从栈顶弹出a3-操作数
4.从栈顶弹出a4-操作符
...
...
...
策略2:补偿保证e1>=e2
当弹出来操作数a1和操作符a2,判断a3,是否a1<a3,如果 a1<a3,则要把a1和a2弹出,重新,入栈一个新的操作数和操作符。直到满足了解,否则继续弹出。
策略3:式子不能重复
1.可以通过将式子字符串进行hash运算,然后存到set中,可能会存在误判,但结果肯定可以保证都是唯一的
**/
static String op1;
static int num1;//生成的题目个数
static String op2;
static int num2;//生成的值的范围[0~num2)
static Random random;
static Stack<Object> st;//用来存放操作数和操作符。自栈顶向下,就是自表达式左向右
static StringBuilder stringBuilder;
static HashSet set;
static StringBuilder HashString;
static int index;
static String[] tokens;
String[] ops={"+","-","*","/"};
static {
num1=3;
num2=2;
random = new Random();
st=new Stack<>();
set=new HashSet();
stringBuilder=new StringBuilder();
tokens=new String[1000];
HashString=new StringBuilder();
}
public void insertOp(){//入操作符
int val = random.nextInt(4);
// stringBuilder.append(ops[val]);
tokens[index++]=ops[val];
}
public void insertNum(){//入操作数
// stringBuilder.append(random.nextInt(num2+1));
tokens[index++]= String.valueOf(FractionGenerator.generateTrueFraction());
}
public void InitData(String[] args){
if(args.length==4){
op1=args[0];
num1=Integer.parseInt(args[1]);
op2=args[2];
num2=Integer.parseInt(args[3]);
}else if(args.length==2){
op1=args[0];
num1=Integer.parseInt(args[1]);
}
}
public void InitTokens(){
tokens=new String[1000];
index=0;
//1.生成运算符的个数
int val=random.nextInt(num2+1);
int count = val==0?1:val;//左闭右开[0~num2)
//2.入栈操作数和操作符
insertNum();
for(int i=0;i<count;i++){
insertNum();// 1 2 + 3 / 4 - 6
insertOp();
}
}
public Double evalRPN(String[] tokens) {
Deque<Double> stack = new LinkedList<Double>();
Deque<String> express = new LinkedList<String>();
express.push(tokens[0]);
int n = index;
for (int i = 0; i < n; i++) {
String token = tokens[i];
if (isNumber(token)) {
stack.push(Double.parseDouble(token));
} else {
Double num2 = stack.pop();
Double num1 = stack.pop();
//
StringBuilder HashString1 = new StringBuilder();
switch (token) {
case "+":
String str1 = express.pop();
HashString1.append("(");
HashString1.append(str1);
HashString1.append("+");
HashString1.append(num2);
HashString1.append(")");
express.push(HashString1.toString());
stack.push(num1 + num2);
break;
case "-":
String str2 = express.pop();
HashString1.append("(");
HashString1.append(str2);
HashString1.append("-");
HashString1.append(num2);
HashString1.append(")");
express.push(HashString1.toString());
stack.push(num1 - num2);
break;
case "*":
String str3 = express.pop();
HashString1.append("(");
HashString1.append(str3);
HashString1.append("*");
HashString1.append(num2);
HashString1.append(")");
express.push(HashString1.toString());
stack.push(num1 * num2);
break;
case "/":
if(num2==0)num2++;
String str4 = express.pop();
HashString1.append("(");
HashString1.append(str4);
HashString1.append("/");
HashString1.append(num2);
HashString1.append(")");
express.push(HashString1.toString());
stack.push(num1 / num2);
break;
default:
}
}
}
HashString=new StringBuilder(express.pop());
return stack.pop();
}
public boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}
public void Run(){
//运行指定的次数
for(int j=0;j<num1;j++){
//1.初始化tokens
InitTokens();
for(int i=0;i<index;i++){
System.out.println(tokens[i]);
}
//2.运算(运算+补偿+拼接表达式)
Double res=evalRPN(tokens);
IOUtils.saveExercises(HashString.toString());
HashString.append("=");
HashString.append(res);
IOUtils.saveAnswers(HashString.toString());
//3.校验表达式是否唯一,不唯一则要重试
if(set.contains(HashString)){
j--;
HashString=new StringBuilder();
continue;
}
set.add(HashString);
//4.完成表达式
System.out.println(HashString.toString());
HashString=new StringBuilder();
}
}
public int caculate(String op,int num1,int num2){
switch (op){
case "+":
return num1+num2;
case "-":
if(num1<num2){
return num2-num1;
}
return num1-num2;
case "*":
return num1*num2;
case "/":
return num1/num2;
default:
return 0;
}
}
static public DataStore getInstance(){
if(dataStore==null){
synchronized (DataStore.class){
if(dataStore==null)
dataStore=new DataStore();
}
}
return dataStore;
}
}
IOUtils.java文件:
IOUtils 类提供了两个静态方法,saveExercises 和 saveAnswers,用于将练习题和答案分别写入不同的文本文件。这种设计允许用户在不创建 IOUtils 类实例的情况下,直接调用这些方法进行文件操作。异常处理确保了在写入文件过程中,如果发生任何输入输出错误,可以得到相关的错误信息以供调试。
源代码:
点击查看代码
package com.lx.utils;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
/**
* IOUtils类用于处理输入和输出操作
* @Author Okg
* @Date 2024/9/28 下午8:01
**/
public class IOUtils {
// 存储题目到文件
public static void saveExercises(String exercises) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("Exercises.txt", true))) {
// 将题目写入文件
writer.write(exercises);
writer.newLine(); // 换行
} catch (IOException e) {
e.printStackTrace(); // 打印异常信息
}
}
// 存储答案到文件
public static void saveAnswers(String answers) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("Answers.txt", true))) {
// 将答案写入文件
writer.write(answers);
writer.newLine(); // 换行
} catch (IOException e) {
e.printStackTrace(); // 打印异常信息
}
}
}
六、运行测试
测试结果:
Exercises.txt文件:
Answers.txt文件:
七、项目总结
- 欧可贵:这是我第一次接触合作开发一个项目,所以有很多合作的部分会比较生疏,本地代码上传不了GitHub、GitHub上的代码拉不下来、java环境配置等等都是需要解决的问题,由于以前使用python较多,所以主要负责帮忙解决I/O功能,以及计算功能中的注释添加,计算功能由李响同学进行开发完成。当然这次结对项目让我认识到了团队协作的重要性,让我们更能清楚团队配合之间主要问题会存在哪里并解决它。
- 李响:本次我主要负责主要功能的代码开发,看到题目我们经过讨论决定先随机生成一个数学表达式,然后使用逆波兰表达式处理各种操作符号,由于代码经验较丰富,所以我负责主要的代码开发。但是由于开发时间过长,破坏了我们原有的打算,效能分析没有能够完成(如果要完成的话可能会导致不能在规定时间内提交作业),我会吸取经验,争取下次做更加合理的时间规划,完成每一个任务要求。