结对编程:四则运算生成器(郑邦坚,吴政毅)
目录
作业要求
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 软件工程 |
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 四则运算生成器项目+合作完成结对项目 |
作业所在Github地址
结对项目成员(学号)
- 吴政毅:3118005339
- 郑邦坚:3118005349
项目思路分析
分数类
生成表达式的原理及思路
1.设计一个生成类,随机产生数和符号,生成后随机加括号,代码如下
public class Generator {
private static final String[] OPERATOR = {"+", "-", "×", "÷"};
private static final Random R = new Random();
public static String generate(int maximum) {
int cnt = R.nextInt(3) + 2;
List<String> exp = new LinkedList<>();
for (int i = 0; i < cnt; i++) {
exp.add(String.valueOf(getFraction(maximum)));
if (i != cnt - 1) {
exp.add(OPERATOR[R.nextInt(4)]);
}
}
if (R.nextInt(9) == 0) {
List<Integer> index = new ArrayList<>();
for (int i = 0; i < exp.size(); i += 2) {
index.add(i);
}
int left = index.get(R.nextInt(index.size() - 1));
int j = 0;
for (int i = 0; i < index.size(); i++) {
if (left < index.get(i)) {
index.set(j++, index.get(i) + 1);
}
}
int right = index.get(R.nextInt(j)) + 1;
exp.add(left, "(");
exp.add(right, ")");//加括号
}
StringBuilder sb = new StringBuilder();
for (String s : exp) {
sb.append(s).append(" ");
}
return sb.toString();
}
public static Fraction getFraction(int maximum) {
//调整随机数为整数或者分数
boolean isFraction = R.nextBoolean();
return isFraction ? new Fraction(R.nextInt(maximum) + 1, R.nextInt(maximum) + 1) : new Fraction(R.nextInt(maximum) + 1, 1);
}
}
生成10000的题目如下:
去重
我们一开始想的是将整个表达式进行存储,每次生成新的式子进行比对,但随着表达式的增多,计算量和空间开销都很大,后来我们想用set集合本身具有的去重的特点去去重,但会存在无法区别1+2+3和3+2+1等类型的情况,我们采取了最简单粗暴的方法,将结果相同的式子删除,代码如下:
计算原理
通过逆波兰表达式的方法计算,逆波兰需要将中缀表达式转为后缀表达式,故书写一个类将将中缀表达式转为后缀表达式,代码如下:
public class Suffix {
public static Map<String, Integer> opValue = new HashMap<>();
static {
opValue.put("×", 1);
opValue.put("÷", 1);
opValue.put("+", 0);
opValue.put("-", 0);
}
public static String toSuffixExp(String exp) {
StringBuilder nums = new StringBuilder();
Stack<String> ops = new Stack();
for (String s : exp.split(" ")) {
if (opValue.containsKey(s)) {
while (!ops.isEmpty() && !ops.peek().equals("(") && (opValue.get(ops.peek()) >= opValue.get(s))) {
nums.append(ops.pop()).append(" ");
}
ops.push(s);
} else if (s.equals("(")) {
ops.push(s);
} else if (s.equals(")")) {
while (!ops.isEmpty()) {
if (ops.peek().equals("(")) {
ops.pop();
break;
} else {
nums.append(ops.pop()).append(" ");
}
}
} else {
nums.append(s).append(" ");
}
}
while (ops.size() > 0) {
nums.append(ops.pop()).append(" ");
}
return nums.toString();
}
}
计算类代码
改为后缀表达式后,进行计算,但由于分数的存在,我们需要区分整数和分数的运算,代码如下:
public class Calculate {
public String calculate(String exp){
Stack<String> st = new Stack<>();
for(String s: exp.split(" ")){
if(Suffix.opValue.containsKey(s)){//是运算符取出栈顶的两个元素进行运算并将结果压入栈
String result;
String fir = st.pop();
String sec = st.pop();
if(isFraction(fir) || isFraction(sec)){
result = fractionCalculate(strToFraction(sec), strToFraction(fir), s);
}else {
result = Calculate(Integer.parseInt(sec), Integer.parseInt(fir), s);
}
// 表达式错误
if(result.equals("-1")) {
st.clear();
return "ERROR";
}
st.push(result);//结果入栈顶
}else{
st.push(s);//操作数入栈
}
}
return st.pop();
}
private boolean isFraction(String s){
return s.contains("/");
}
public Fraction strToFraction(String s){
int numerator, denominator;
if (s.contains("'")){
denominator = Integer.parseInt(s.split("/")[1]);
String[] split = s.split("/")[0].split("'");
numerator = Integer.parseInt(split[0]) * denominator + Integer.parseInt(split[1]);
}else if(s.contains("/")){
//真分数
numerator = Integer.parseInt(s.split("/")[0]);
denominator = Integer.parseInt(s.split("/")[1]);
}else{
//整数
numerator = Integer.parseInt(s);
denominator = 1;
}
return new Fraction(numerator, denominator);
}
/*整数运算*/
public String Calculate(int num1, int num2, String op){
int result = -1;
switch (op){
case "+":
result = num1 + num2;
break;
case "-":
if(num1 >= num2){
result = num1 - num2;
}
break;
case "×":
result = num1 * num2;
break;
case "÷":
default:
// 分母不能为0
if(num2 == 0) return String.valueOf(-1);
if(num1 % num2 == 0){
result = num1 / num2;
}else {
return new Fraction(num1, num2).toString();
}
}
return String.valueOf(result);
}
/*分数运算*/
public String fractionCalculate(Fraction fraction1, Fraction fraction2, String op){
// 获取分数的分子、分母
int Numerator1 = fraction1.getNumerator();
int Denominator1 = fraction1.getDenominator();
int Numerator2 = fraction2.getNumerator();
int Denominator2 = fraction2.getDenominator();
// 新分数的分子、分母
int Numerator3, Denominator3;
switch (op){
case "+":
Numerator3 = Numerator1 * Denominator2 + Numerator2 * Denominator1;
Denominator3 = Denominator1 * Denominator2;
break;
case "-":
//计算过程不能产生负数
if((Numerator3 = Numerator1 * Denominator2 - Numerator2 * Denominator1) < 0){
return String.valueOf(-1);
}else{
Denominator3 = Denominator1 * Denominator2;
}
break;
case "×":
Numerator3 = Numerator1 * Numerator2;
Denominator3 = Denominator1 * Denominator2;
break;
case "÷":
default:
Numerator3 = Numerator1 * Denominator2;
Denominator3 = Denominator1 * Numerator2;
//分母不能为0
if(Denominator3 == 0) return String.valueOf(-1);
}
if(Numerator3 % Denominator3 == 0){//运算结果为整数
return String.valueOf(Numerator3 / Denominator3);
}
return new Fraction(Numerator3, Denominator3).toString();
}
}
文件流的输入输出类和统计成绩结果输出
文件流的输入输出相对简单,统计成绩结果原理则是计算式子的答案,并与输入的答案进行比对,代码如下:
测试的结果如下:
改错1.2题的答案
测试类的代码:
public class unitTest {
private Calculate calculate = new Calculate();
/**
* 测试生成题目
*/
@Test
public void testGenerator(){
System.out.println(Generator.generate(10));
}
/**
* 测试写入文件
*/
@Test
public void testWriteFile(){
FileUtil.write("Exercises.txt", Generator.generate(10));
}
/**
* 测试分数四则运算
*/
@Test
public void testFractionArithmetic(){
Fraction f1 = new Fraction(1, 3);
Fraction f2 = new Fraction(1, 4);
String operator = "÷";
String s = calculate.fractionCalculate(f1, f2, operator);
assert(s.equals("1'1/3"));
}
/**
* 测试将中缀表达式转换为后缀表达式
*/
@Test
public void testToSuffixExp(){
String exp = "9 - 4 × ( 2 ÷ 5 )";
assert(Suffix.toSuffixExp(exp).equals("9 4 2 5 ÷ × -"));
}
/**
* 测试计算类
*/
@Test
public void testCalculate(){
String exp = "9 - 4 × ( 2 ÷ 5 )";
String s = Suffix.toSuffixExp(exp);
assert(calculate.calculate(s).equals("7'2/5"));
}
健壮性
错误输入会报错
性能分析:
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 37 |
Estimate | 估计这个任务需要多少时间 | 30 | 23 |
Development | 开发 | 500 | 600 |
Analysis | 需求分析(包括学习新技术) | 60 | 80 |
Design Spec | 生成设计文档 | 40 | 79 |
Design Review | 设计复审 | 20 | 17 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 40 |
Design | 具体设计 | 30 | 45 |
Coding | 具体编码 | 20 | 22 |
Code Review | 代码复审 | 30 | 21 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 42 |
Reporting | 报告 | 30 | 45 |
Test Repor | 测试报告 | 30 | 40 |
Size Measurement | 测试工作量 | 20 | 45 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 20 | 30 |
Total | 合计 | 920 | 1166 |
总结
优点
1.经过这个结对项目,我们不再只是局限于原先的个人项目的各自开发,而是作为一个团队来一块开发这个项目。结对编程让我们的工作更有动力,能够集思广益减少犯错误的几率
2.遇到问题时,能够彼此有不同的思路,如我们遇到去重的问题时,我们提出了方案,但实用性并不好,这是另一种思路就显得格外的重要,从而有1+1>2的效果
缺点与不足
1.因为线下能够一起编码,且时间比较赶,就比较少使用git的版本迭代工具
2.没有形成属于自己的编码规范,这也与第一点有关,因为线下,几乎编码两个人都是能够理解的就没有形成较好的规范,后续可以通过codeReview来弥补一下