软工项目二:结对项目
一、GitHub地址:https://github.com/hhhh344/Arithmetic
项目合作者:3118005001 胡鹤腾 3118005003 黄济成
项目使用网址:http://120.78.187.151:8081/
(注:因为校验答案的算法有bug,如果上传了不合规格的文件就会炸,所以这里面只有生成表达式的功能)
二、题目叙述
2.1题目数字以及运算符要求:
- 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 自然数:0, 1, 2, …。
- 运算符:+, −, ×, ÷。
- 括号:(, )。
- 等号:=。
- 分隔符:空格(用于四则运算符和等号前后)。
- 算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
- 四则运算题目:e = ,其中e为算术表达式。
2.2 生成题目具体操作过程及格式:
- 使用 -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
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
需求实现
需求描述 | 是否实现 |
---|---|
控制生成题目的个数 | 是 |
控制题目中数值范围 | 是 |
计算过程不能产生负数 | 是 |
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数 | 是 |
每道题目中出现的运算符个数不超过3个 | 是 |
程序一次运行生成的题目不能重复 | 否 |
生成的题目存入执行程序的当前目录下的Exercises.txt文件 | 是 |
计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件 | 是 |
程序应能支持一万道题目的生成 | 是 |
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计 | 是 |
三、psp表
*PSP2.1* | *Personal Software Process Stages* | *预估耗时(分钟)* | *实际耗时(分钟)* |
---|---|---|---|
Planning | 计划 | 10 | 10 |
·Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 1440 | 2670 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 500 |
· Design Spec | · 生成设计文档 | 60 | 60 |
·Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
·Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 180 | 360 |
· Coding | · 具体编码 | 600 | 1000 |
· Code Review | · 代码复审 | 100 | 200 |
· Test | · 测试(自我测试,修改代码,提交修改) | 200 | 360 |
Reporting | 报告 | 120 | 120 |
· Test Report | · 测试报告 | 60 | 60 |
·Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1570 | 2730 |
四、效能分析
1 程序效能
2.消耗最多的函数
五、设计实现过程
六、代码说明
最主要的是先将表达式定义好,我们自定义的表达式如下
public class Expression {
/**
* parameterList 操作数列表
*/
private List<Integer[]> parameterList;
/**
* operatorList 操作符列表
*/
private List<String> operatorList;
/**
* result 运算结果
*/
private Integer[] result;
/**
* pattern 表达式模型
*/
private String pattern;
1、ExpressionDaoImpl类里面的几个方法
表达式定义好后,实现起来不难,这里就不贴代码了
将生成的表达式转化为字符串类型,方便将其存储再入栈或者写入文件
public String expressionToString(Expression exp) {
String pattern = exp.getPattern();
//要被返回的字符串
String returnString = "";
List<Integer[]> parameterList = exp.getParameterList();
Integer[] num;
List<String> operatorList = exp.getOperatorList();
String operator;
int parameterIndex = 0;
int operatorIndex = 0;
//每一个表达式都存储着其模型,按照模型将其转化
for(int i = 0; i < pattern.length(); i++) {
char temp = pattern.charAt(i);
switch (temp) {
case '(':
case ')':
returnString += temp + " ";
break;
case 'n':
num = parameterList.get(parameterIndex++);
if(num[0] == 0) {
returnString += num[1] + " ";
}
else {
//如果带分数前面的整数为0,则只打印分数部分
if(num[1] != 0) {
returnString += num[1] + "'" + num[2] + "/" + num[3] + " ";
}
else {
returnString += num[2] + "/" + num[3] + " ";
}
}
break;
case '#':
operator = operatorList.get(operatorIndex++);
if(operator.contains("/")) {
returnString += "÷ ";
}
else {
returnString += operator + " ";
}
break;
default:
}
}
returnString += "=";
return returnString;
}
将字符串类型的表达式转化为自定义的表达式,用于读取文本文件里的表达式
public Expression stringToExpression(String expressionString) {
Expression exp = new Expression();
List<Integer[]> parameterList = new ArrayList<>();
List<String> operateList = new ArrayList<>();
String pattern = "";
String[] str = expressionString.split("\\s");
for (String item : str) {
if(item.matches("^[()]$")) {
pattern += item;
}
else if(item.matches("^[\\+\\-\\*÷]$")) {
if("÷".equals(item)) {
operateList.add("/");
}
else {
operateList.add(item);
}
pattern += "#";
}
//整数
else if(item.matches("^[0-9]+$")) {
pattern += "n";
Integer[] num = new Integer[2];
num[0] = 0;
num[1] = Integer.parseInt(item);
parameterList.add(num);
}
//分数
else if(item.matches("^([0-9]+')?[0-9]+\\/[0-9]+$")) {
pattern += "n";
Integer[] num = new Integer[4];
num[0] = 1;
if(item.contains("'")) {
num[1] = Integer.parseInt(item.substring(0, item.indexOf("'")));
num[2] = Integer.parseInt(item.substring(item.indexOf("'")+1, item.indexOf("/")));
}
else {
num[1] = 0;
num[2] = Integer.parseInt(item.substring(0, item.indexOf("/")));
}
num[3] = Integer.parseInt(item.substring(item.indexOf("/")+1));
parameterList.add(num);
}
else if(item.matches("^=$") || !" ".equals(item)) {
}
else {
throw new RuntimeException("将字符串表达式转化为Expression时出现了未知字符" + item);
}
}
exp.setOperatorList(operateList);
exp.setParameterList(parameterList);
exp.setPattern(pattern);
return exp;
}
2、CalculateUtilsImpl类里面的几个方法
将表达式转化为后缀表达式
public Stack<String> getPostfixExpression(Expression expression) {
Stack<String> S1 = new Stack<>();
Stack<String> S2 = new Stack<>();
List<Integer[]> parameterList = expression.getParameterList();
Integer[] num;
List<String> operatorList = expression.getOperatorList();
String operator;
int parameterIndex = 0;
int operatorIndex = 0;
//获取表达式的模式
String pattern = expression.getPattern();
S1.push("#");
for(int i = 0; i < pattern.length(); i++) {
char temp = pattern.charAt(i);
switch (temp) {
//若取出的字符是操作数,则分析出完整的运算数,该操作数直接送入S2栈
case 'n':
num = parameterList.get(parameterIndex++);
if(num[0] == 0) {
S2.push(num[1].toString());
}
else {
S1.push("(");
S2.push(num[1].toString());
S1.push("+");
S2.push(num[2].toString());
S1.push("/");
S2.push(num[3].toString());
while(S1.peek() != "(") {
S2.push(S1.pop());
}
S1.pop();
}
break;
case '#':
operator = operatorList.get(operatorIndex);
//1.如果S1为空,或栈顶为"(",则将该运算符进S1栈
if(S1.peek() == "#" || S1.peek() == "(") {
S1.push(operator);
operatorIndex++;
}
//2.如果该运算符优先级(不包括括号运算符)大于S1栈栈顶运算符优先级,则将该运算符进S1栈
else if(comparePriority(operator, S1.peek())) {
S1.push(operator);
operatorIndex++;
}
//3.否则,将S1栈的栈顶运算符弹出,送入S2栈中,跳回1
else {
S2.push(S1.pop());
i--;
}
break;
//若取出的字符是“(”,则直接送入S1栈顶。
case '(':
S1.push("(");
break;
//若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
case ')':
while(S1.peek() != "(") {
S2.push(S1.pop());
}
if(S1.peek() == "(") {
S1.pop();
}
default:
}
}
while(S1.peek() != "#") {
S2.push(S1.pop());
}
return S2;
}
传进一个表达式,计算表达式结果
public Integer[] getExpressionResult(Expression expression) {
//将表达式转化为后缀表达式
Stack<String> postfixExpression = getPostfixExpression(expression);
ExpressionDaoImpl exp = new ExpressionDaoImpl();
Stack<Integer[]> S3 = new Stack<>();
Integer[] num1, num2, temp;
for (String item : postfixExpression){
// 如果取出的元素是数字
if(item.matches("[0-9]+")){
S3.push(toInteger(item));
}
// 如果取出的元素是操作符
else if (item.matches("[\\+\\-\\*\\/]")){
// 栈顶元素应该在操作符后面
num2 = S3.pop();
num1 = S3.pop();
temp = calculateTwoNumber(num1,num2,item);
// 如果两个数字不符合计算规则,除法出现被除数为零,减法出现负数
if(temp[0]==4){
return temp;
}
S3.push(temp);
}
}
if (S3.size()!=1){
throw new RuntimeException("栈内元素剩余不等于1!" + S3.toString());
}
//将最终结果换为真分数
Integer[] result = exp.getProperFraction(S3.peek());
return result;
}
3、FileUtilsImpl类里面的几个方法
读取文本文件时,将题号和表达式使用Map<>映射起来
public Map<Integer, String> getExpressionFileMap(File expressionFile) throws IOException {
FileReader fr = new FileReader(expressionFile);
BufferedReader br = new BufferedReader(fr);
Map<Integer, String> expressionFileMap = new HashMap<>();
String line = br.readLine();
Integer number;
Integer[] result;
Expression expression;
while(line != null && line != "\n") {
number = Integer.parseInt(line.substring(0, line.indexOf(".")));
expression = exp.stringToExpression(line.substring(line.indexOf(".")+1));
result = cal.getExpressionResult(expression);
expressionFileMap.put(number, cal.resultToString(result));
line = br.readLine();
}
br.close();
fr.close();
return expressionFileMap;
}
读取文本文件时,将题号和答案使用Map<>映射起来
public Map<Integer, String> getAnswerFileMap(File answerFile) throws IOException {
FileReader fr = new FileReader(answerFile);
BufferedReader br = new BufferedReader(fr);
Map<Integer, String> answerFileMap = new HashMap<>();
String line = br.readLine();
Integer number;
String[] answerString;
while(line != null && line != "\n") {
answerString = line.split(" ");
number = Integer.parseInt(answerString[0].substring(0, answerString[0].indexOf(".")));
if (answerString.length == 2) {
answerFileMap.put(number, answerString[1]);
}
line = br.readLine();
}
br.close();
fr.close();
return answerFileMap;
}
比较表达式文件里面的表达式和答案文件里面的答案是否相同,并将统计结果写入Grade
public boolean writeGradeInFile(File expressionFile, File answerFile, File gradeFile) throws IOException {
FileWriter fw = new FileWriter(gradeFile);
BufferedWriter bw = new BufferedWriter(fw);
Map<Integer, String> expressionFileMap = getExpressionFileMap(expressionFile);
Map<Integer, String> answerFileMap = getAnswerFileMap(answerFile);
int correctCount = 0;
int wrongCount = 0;
String correctString = "Correct:(";
String wrongString = "Wrong:(";
for (Map.Entry<Integer, String> item : expressionFileMap.entrySet()) {
Integer key = item.getKey();
//比较两个答案字符串是否一致
if(item.getValue().equals(answerFileMap.get(key))) {
correctCount++;
if(correctCount == 1) {
correctString += key;
}
else {
correctString += ", " + key;
}
}
else {
wrongCount++;
if(wrongCount == 1) {
wrongString += key;
}
else {
wrongString += ", " + key;
}
}
}
correctString += ")";
wrongString += ")";
StringBuilder correctStringBuilder = new StringBuilder(correctString);
StringBuilder wrongStringBuilder = new StringBuilder(wrongString);
correctStringBuilder.insert(correctStringBuilder.indexOf("("), correctCount);
wrongStringBuilder.insert(wrongStringBuilder.indexOf("("), wrongCount);
bw.write(correctStringBuilder.toString());
bw.newLine();
bw.write(wrongStringBuilder.toString());
bw.close();
fw.close();
if(wrongCount == 0) {
return true;
}
return false;
}
七、测试运行
1、如图所示,可以随机生成一万道题,数据范围为10(可用取值范围为2~10000),人工验证计算出的答案是正确
2、校验答案
和Grade文件里面的结果一致
八、项目小结
- 按照结对项目的要求,整个项目的完成一直都是两个人合作实现的。
- 第一天的时候并没用直接写代码,而是先分析了该项目需求,讨论了该创建哪种项目模式,和实体类的定义。
- 第一次使用Java完成一个项目,一开始的时候很不熟练,所以前面的时候有不少时间在纠结创建哪些类,和哪些包,以及它们之间的关系。
- 主要遇到的问题有以下几个:
- 如何存储这些随机生成的数字,主要是分数
- 如何随机生成括号并且没有错误或者无意义
- 计算的时候因为分数要通分,导致的通分后分母可能过大而出现的溢出问题
- 判重问题(没有解决)
- 在git上遇到不少的问题,包括创建一个团队仓库,直接在IDEA上git,还有遇到的最大的麻烦就是合并分支出现的冲突问题,我们创建了一个develop分支,主要是在这个分支上提交,但我们也修改过master,所以在将其合并到master上的出现了冲突
5、个人总结:
黄济成:收获,这一次项目是先规划了大部分内容,包括先写好了接口,写好注释,然后接下来才是开始写码实现,所以在中间实现的时候很流畅,只要去实现那些功能就可以了。然后就是写好一个功能后就会去写单元测试,虽然这个花了不少时间,但的确也发现了不少问题。实际开发时间远比规划时间多的主要原因是没有料到那些意外(比如git)会花如此多的时间。这种结对合作,让我在写代码的时候更集中注意力,遇到问题的时候更是能集中两个人的智慧解决,这次的项目十分感谢我的合作伙伴,让我学到了非常多的东西。
胡鹤腾: 整个项目开发过程中我们花了比较多的时间去学习、构思项目结构的构建,git以及合作的方法。一开始要进行不能面对面的合作交流是比较困难的,后来我们适应了用腾讯会议来交流思路、互相监督以及用git共同管理仓库,这还是足够便利的。这些合作技能也是我在这次项目中最大的收获,还有一些收获是编程能力的提高、学习能力的提高。遗憾的是代码写得不是很规范,第一次使用JAVA写完整的项目,我们后来意识到这点但也难以改动,但是下次会做得更高效、规范。