结对项目
结对作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | < 软件工程 > |
这个作业要求在哪里 | < 作业要求 > |
这个作业的目标 | < 学会与搭档分工合作,完成一个项目 > |
一、前言
github地址:点击进入
姓名 | 学号 |
---|---|
刘晓霖 | 3118005374 |
任浩然 | 3118005381 |
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 600 | 720 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 100 |
· Design Spec | · 生成设计文档 | 20 | 15 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 40 | 60 |
· Coding | · 具体编码 | 480 | 420 |
· Code Review | · 代码复审 | 40 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 150 |
Reporting | 报告 | 60 | 60 |
· Test Report | · 测试报告 | 60 | 40 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 合计 | 1670 | 1720 |
三、项目设计
1.类的介绍
(1)运算数Number类
由于生成的题目中,分数要以“a/b”的形式表示,同时为了方便运算,我们决定自定义一个Number来表示数字。
该类中拥有分子和分母两个属性,并利用辗转相除法,在toString()方法中把数字化为最简的真分数形式,以方便
形成题目的字符串。
public class Number {
//分子
private Integer numerator;
//分母
private Integer denominator;
//getter & setter
...
public String toString() {
String s = null;
Integer gcdNum = gcd(numerator,denominator);
numerator = numerator / gcdNum;
denominator = denominator / gcdNum;
if (denominator == 1) {
s = numerator.toString();
} else if (numerator < denominator) {
s = numerator + "/" + denominator;
} else if (numerator > denominator) {
Integer i = numerator / denominator;
Integer m = numerator % denominator;
if(m != 0){
s = i + "'" + m + "/" + denominator;
}else {
s = i.toString();
}
}else if(numerator == denominator){
s = "1";
}
return s;
}
(以上为Number类部分代码)
(2)符号Symbol类
为了实现在运算过程中,有括号的情况下按优先级进行运算,需要为四个运算符号设置优先级。
同时为了实现后面的检测重复题目的实现,还需要为每个符号设置一个序号,以方便排序。
public abstract class Symbol {
//符号优先级
public int priotiry;
//符号名称
public String name;
//符号序号
public Float no;
//getter & setter
...
}
Symbol是一个抽象类,四个运算符(AddSyml,SubSyml,MulSyml,DivSyml)都继承于它。
(3)生成问题与计算CreateProblems类
该类是项目的核心,题目生成、运算和排除重复功能都在此类。稍后详细介绍里面各个方法。
2.实现步骤
(1)实现思路
生成3条 List,分别为用来存储运算数的 List
每次运算时,先在 bracList 中取出所有值,然后再在opList中找到以这些值为序号的符号,从中选出优先级最高的一个,再从
numList中把该符号的左右两数取出,进行运算。如果 bracList 为空,则从 opList 第一个值开始,遍历出优先级最大的符号(优先级
一样则选最先出现的),进行运算。运算完之后,还要对修改3条 List 的数据,然后重复以上步骤直至 opList 为空。
例如
(1)从bracList得到括号包含的运算符位置为0,1.所以从opList中序号0到1范围找出优先级最大的一个,这里是"X",序号是1。再从numList中找到序号为1和1+1(即2)的数,进行一次运算。
(2)经过一次运算后,把当前运算符的序号记为 pos,再把opList和bracList表中序号为 pos 的元素去除。把计算的新值代替 numList 中第 pos 个元素,删除 pos + 1 的元素,得到上图的表。
(3)重复以上步骤,可得到以上表。此时 bracList 已为空,所以 opList 从第一个元素开始找优先级最高的一个,继续重复以上的运算步骤。得到最后结果 11'1/2 .
(2)问题生成
1.首先有一个 randomNum(int range) 的方法,该方法以 range 为范围,会生成一个随机数。
2.在生成问题的 createProblem()方法中,首先利用以上函数,生成运算数的个数 num,然后运算符个数opNum = num - 1。括号内包含运算符的个数 bracNum 则由 randomNum(opNum) 得到。
3.根据上一步骤得到的各个数,随机生成相对数量的数字、运算符、括号位置。
4.由以上步骤,可得到3个List。再利用 StringBuilder 进行拼接,即可得到问题的字符串。
public String createProblem(Integer range) throws Exception {
...
//随机生成参与运算的数
for (int i = 0; i < num; i++) {
Integer numerator = randomNum(range - 1) + 1;
Integer denominator = randomNum(range - 1) + 1;
Number number = new Number();
number.setNumerator(numerator);
number.setDenominator(denominator);
numList.add(number);
numList2.add(number);
}
//随机生成运算符
for (int i = 0; i < opNum; i++) {
Integer j = randomNum(4);
Symbol symbol = null;
switch (j) {
case 0:
symbol = new AddSyml();
break;
case 1:
symbol = new SubSyml();
break;
case 2:
symbol = new MulSyml();
break;
case 3:
symbol = new DivSyml();
break;
}
opList.add(symbol);
opList2.add(symbol);
}
//生成问题字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < num; i++) {
//为方便拼接字符串,把所有的数字都替换为r
if(i == num - 1){
sb.append(" " + "r ");
}else {
sb.append(" " + "r");
}
try {
sb.append(" " + opList.get(i).getName());
} catch (Exception e) {
}
}
//插入括号
try{
Integer bracFrom = bracList.get(0);
Integer bracTo = null;
if (bracNum == 2) {
bracTo = bracList.get(1);
}
if (bracNum == 1) {
sb.setCharAt((bracFrom * 4) + 3 - 3, '(');
sb.setCharAt((bracFrom * 4) + 3 + 3, ')');
} else if (bracNum == 2) {
sb.setCharAt((bracFrom * 4) + 3 - 3, '(');
sb.setCharAt((bracTo * 4) + 3 + 3, ')');
}
}catch (Exception e){
}
//从字符串的“r”分裂字符串成多个子串,再与数字一起拼接成完整的字符串
String str = sb.toString();
String[] rs = str.split("r");
StringBuilder builder = new StringBuilder();
for(int i = 0;i < rs.length;i++){
if( i == rs.length - 1){
builder.append(rs[i]);
}else {
builder.append(rs[i]).append(numList.get(i));
}
}
builder.append("= ");
//格式化字符串
String result = builder.toString();
result = result.replace("("," ( ");
result = result.replace(")"," ) ").trim();
return result;
...
}
(3)运算
在一个问题生成之后,可利用那3条 List 表,按照上面的思路,即可实现运算。值得一提的是,由于不允许计算中出现负数,因此在每一次子运算中,如果结果是负数,就跳出该次循环。
while (opList.size() != 0) {
sum = new Number();
//括号起始位置
Integer from = null;
//括号结束位置
Integer to = null;
try {
from = bracList.get(0);
to = bracList.get(bracList.size() - 1);
} catch (Exception e) {
}
//第一个运算数
Number num1 = null;
//第二个运算数
Number num2 = null;
//符号起始位置
Integer opFrom;
//符号结束位置
Integer opTo;
if (from != null) {
//如果存在括号,那么符号要在括号内寻找优先级最高的
opFrom = from;
opTo = to;
} else {
//如果不存在括号,从头开始找优先级最高的符号
opFrom = 0;
opTo = opList.size() - 1;
}
//最大的优先级,默认括号内第一个符号
Integer maxPriority = null;
try{
maxPriority = opList.get(opFrom).getPriotiry();
}catch (Exception e){
return null;
}
//优先级最大的符号的位置,默认括号内第一个符号
Integer maxPos = opFrom;
//目前运算符的优先级
Integer priority = null;
//目前运算符的位置
Integer pos = null;
//寻找当前运算中,优先级最高的符号以及其位置。
for (int i = opFrom; i <= opTo; i++) {
try{
priority = opList.get(i).getPriotiry();
}catch (Exception e){
return null;
}
pos = i;
if (priority > maxPriority) {
maxPriority = priority;
maxPos = pos;
}
}
//分子
Integer numerator = null;
//分母
Integer denominator = null;
//把运算符左右两边的数取出来
num1 = numList.get(maxPos);
num2 = numList.get(maxPos + 1);
String symbol = opList.get(maxPos + 0).getName();
//计算过程
if (symbol.equals("+")) {
denominator = num1.getDenominator() * num2.getDenominator();
Integer num1Numerator = num1.getNumerator() * num2.getDenominator();
Integer num2Numerator = num2.getNumerator() * num1.getDenominator();
numerator = num1Numerator + num2Numerator;
} else if (symbol.equals("−")) {
denominator = num1.getDenominator() * num2.getDenominator();
Integer num1Numerator = num1.getNumerator() * num2.getDenominator();
Integer num2Numerator = num2.getNumerator() * num1.getDenominator();
numerator = num1Numerator - num2Numerator;
} else if (symbol.equals("×")) {
numerator = num1.getNumerator() * num2.getNumerator();
denominator = num1.getDenominator() * num2.getDenominator();
} else if (symbol.equals("÷")) {
numerator = num1.getNumerator() * num2.getDenominator();
denominator = num1.getDenominator() * num2.getNumerator();
}
sum.setNumerator(numerator);
sum.setDenominator(denominator);
if(symbol.equals("÷") && numerator <= 0 || denominator == 0){
//除数不能为0
return null;
}
if(numerator < 0){
//计算过程出现负数,不符合要求
return null;
}
if(opList.size() == 1){
//获取计算的最后一步,以方便验证是否题目重复
if(num1.getValue() > num2.getValue() && (symbol.equals("+")||symbol.equals("×"))){
resultStorage = num2.toString() + symbol + num1.toString();
}else {
resultStorage = num1.toString() + symbol + num2.toString();
}
}
//一次运算之后,修改各List数据
numList.set(maxPos, sum);
try {
numList.remove(maxPos + 1);
opList.remove(maxPos + 0);
bracList.remove(maxPos);
} catch (Exception e) {
}
}
(4) 检测重复
首先,在每生成一道题目的时候,都会生成3条 List 来存储问题的信息。在这里我们再用2条 List(numList2 和 opList2)来复制 numList 和 opList ,来保存一个问题的原始数据(因为 numList 和 opList 会在运算过程中发生改变)。同时我们还需要存储运算的最后一步(即opList只剩一个值时),比如上面的例子中,要存储 “23 ÷ 2” 到叫 resultStorage 的字符串中。值得一提的是,如果最后一步是加法或乘法,需要对两位数字进行排序,因为 a+b 、axb 等价于 b+a、bxa,但 a-b 、a÷b不等价于 b-a b÷a。
我们知道,如果两道题目是重复的,那么它们的 numList2 、 opList2 、 resultStorage 是一样的。但 List 里面值的顺序不一样是难以相比较的,因此我们利用两条 List 的值或序号(前面定义Symbol时提到过),对它们进行排序。最后把两条 List 和 resultStorage 的值拼接成一个字符串 problemInfo(比如上面的例子,problemInfo是“2 3 4 5 + x ÷ 23÷2”,再存入 Map<String,Boolean> storage 中。以后每生成一条题目时,进行一次 storage.get(problemInfo) 操作,如果返回值为 null,则没有重复。如果返回值不为空,则表明重复,丢弃该题。
虽然该方法有一个小瑕疵,有可能出现不重复的题目判定为重复,但概率极低(需要数字一样,运算符一样,最后一步一样),而且不会出现重复的题目。
3.异常处理
1.如果输入的文件路劲不存在,则会提示文件不存在。
2.如果输入的取值范围不是正整数,则会提示重新输入。
4.使用测试
(传入的答案文件,为了方便这里直接用了Answer.txt)
(再自己写一个txt,故意把第6、7、9题答案写错)
四、性能分析
五、个人总结
在初见项目要求时,觉得十分简单。但在具体设计的时候,还是发现有很多细节需要实现,并没那么简单。这次作业要求与另一位同学进行合作,也是我从来没试过的,一直以来都是自己做。在这过程中,我也收获了很多。在交流思路的过程中,双方的点子相碰撞,激发了我们更多的灵感。同时,在交流的过程中,如果自己的思路有漏洞,对方能即使反馈,能省下不少试错的时间。
--刘晓霖
这次作业,我们看了题目,商量了大概的框架之后,进行了明确的分工,也让我们培养了独自完成一个模块的能力,也锻炼了我们的团队合作意识,所以团队作业是一个很好锻炼自己能力的机会。
--任浩然