结对编程练习_四则运算(第二周)
一、需求分析
实现一个命令行程序,要求:
自动生成小学四则运算题目(加、减、乘、除)
- 支持整数
- 支持多运算符(比如生成包含100个运算符的题目)
- 支持真分数
- 统计正确率
- 能生成随机数
- 产生的算式要有括号
- 能支持分数的运算
- 要建立堆栈,进行中缀转后缀,以及后续后缀的运算
- 能输入想要产生的题目数
- 能输入用户计算的答案
- 能够比较用户输入的答案是否正确
- 能够统计用户答题的正确率
二、设计思路
- 生成一个有加减乘除支持括号的算式,以字符串的形式输出,每个操作数或操作符中间都用空格隔开。
- 生成分数,将其化成最简分数
- 先生成一个不带括号的算式
- 将生成的不带括号的算式随机插入括号
- 然后调用String类中的split方法,将字符串转化为字符串数组。
- 使用中缀表达式转后缀表达式规则将中缀表达式形式的字符串数组以后缀表达式的形式储存在堆栈中。
- 用后缀表达式计算规则进行计算,得出结果
- 得出的结果与用户输入结果进行比较
- 计算出正确率
三、实现过程中的关键代码解释
- 代码1:
import java.util.Random;
int length = randnum.nextInt(7) + 2;
- 功能:随机生成操作数的个数,包含length个操作数,length>=2。
- 解析:
- 代码2:
str = s.split(" ");
- 功能:将字符串转化为字符串数组,以空格作为分隔符。
- 解析:
- 代码3:
int countLBracket = 0;//记录产生的左括号的个数
boolean choicechar;//选择是否要生成括号
for (int i = 0; i < str.length; i = i + 2) {
choicechar = randnum.nextBoolean();//是否产生左括号
if (choicechar && i != str.length - 1) {
str[i] = "( " + str[i];
countLBracket++;
}
choicechar = randnum.nextBoolean();//是否产生右括号
if (choicechar && !str[i].startsWith("(") && countLBracket != 0) {
str[i] = str[i] + " )";
countLBracket--;
}
}
- 功能:生成随机生成括号,括号位置正确,而且能支持嵌套。
- 解析:我们对上周的代码进行优化,我的小伙伴机智的用
if (choicechar && !str[i].startsWith("(") && countLBracket != 0)
一句解决了生成的括号可能产生这种的情况:(8)+34
。 - 代码4:
while (tokenizer.hasMoreTokens()){
token=tokenizer.nextToken();
if (isOperator(token)){
if (!OpStack.empty()){
if(judgeValue(token)>judgeValue(OpStack.peek()) && !token.equals(")") || token.equals("("))
OpStack.push(token);
else if (token.equals(")")){
//如果遇到一个右括号则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止
while (!OpStack.peek().equals("("))
output=output.concat(OpStack.pop()+" ");//弹出左括号上面的所有东西
OpStack.pop();//弹出左括号
}
else {
while (!OpStack.empty() && judgeValue(token)<=judgeValue(OpStack.peek())){
////如果遇到其他任何操作符,从栈中弹出这些元素直到遇到发现更低优先级的元素或栈空为止
output=output.concat(OpStack.pop()+" ");
}
OpStack.push(token);
}
}
else
OpStack.push(token);//如果栈空则直接将遇到的操作符送入栈中,第一个不可能为右括号
}
else {
output=output.concat(token+" ");//如果遇到操作数就直接输出
}
}
while (!OpStack.empty()){
//如果读到了输入分末尾,则将占中所有元素依次弹出
output=output.concat(OpStack.pop()+" ");
}
- 功能:将中缀表达式转后缀表达式
- 转化方法:
- 如果遇到操作数就直接输出。
- 如果遇到操作符则将其放入栈中,遇到左括号也将其放入
栈中。 - 如果遇到右括号,则将栈元素弹出,将弹出的操作符输出直到遇到遇到左括号为止,注意左括号只弹出不输出。
- 如果遇到任何其他操作符,如“+”,“-”,“*”,“÷”,“(”等,从栈中弹出元素直到遇到
更低优先级的元素或栈空为止。弹出完这些元素才将遇到的操作符压入栈中。 - 如果读到了输入的末尾,则将栈中所有元素依次弹出。
- 解析:请看上方代码中注释。
- 代码5:
while (tokenizer.hasMoreTokens()){
token=tokenizer.nextToken();
if(isOperator(token)){
op2=stack.pop();
op1=stack.pop();
result=calcSingle(op1,op2,token);
stack.push(new Integer(result));
}
else {
stack.push(new Integer(token));
}
- 功能:使用后缀表达式规则进行计算。
- 解析:参考后缀表达式计算规则
- 代码6:
try {
……
}catch (ArithmeticException e){
……
}
- 功能:解决生成的算式中除数是0的问题。
- 解析:参考try-catch的使用方法
- 代码7
int gcdivisor = gcd(x,y);
if (y/gcdivisor!=1) {//产生真分数
s = s + x / gcdivisor + '/' + y / gcdivisor;
}
else {
s = s + x / gcdivisor;
}
- 功能:保证产生的分数为最简分数。
- 解析:x代表分子,y代表分母。x/y:d = gcd(x,y),x/y=(x/d)/(y/d),若若分母不为1,则x/y不是整数;否则x/y为整数。
- 代码8
if (op1.matches(".*/.*")){
dominator1=Integer.valueOf(op1.split("/")[1]);
numerator1=Integer.valueOf(op1.split("/")[0]);
} else {
numerator1=Integer.valueOf(op1);
}
- 功能:将分数的分子与分母分开,为后面的计算做铺垫。
- 解析:
- 该代码用到了String类中的public boolean matches(String regex)方法,详情请见教材187页。
- 找到与分数匹配的表达式后用了一个字符序列的分解split将分数分解成两个部分:分子,分母。
- 分子在字符串数组中的[0]位置,分母在[1]位置,然后再用valueOf将字符串转化为数组。
四、UML类图
五、运行结果截图
六、Junit测试
测试结果截图:
七、代码提交
码云链接: https://gitee.com/imjoking/PairWork
八、遇到的困难及解决方法
-
问题1:如何生成最简分数?
-
解决方法:先生成任意的分数,再对其求最大公因子,分别除以最大公因子,即求得最简分数。
-
问题2:生成的是头尾括号而且头尾括号所包含的内容是式子的全部内容。
-
解决方法:此问题还未能解决。
-
问题4:测试过程中发现问题。
- (1) 使用switch-case忘记加break,导致输出结果与期待值不符。
- 运行结果截图:
- (2)对于队友的代码没有十分清楚的了解,所以对代码进行测试的时候,出现了fail。
- 测试结果截图:
- 错误原因:认真对比源代码与测试代码找出出现fail的原因。经过对比,我发现源代码中的output是在方法前定义的,所以进行方法多次调用所产生的output是对前面几次调用产生所结果的叠加。
- 源代码截图:
- 测试代码截图:
- 解决方法:
- 方法一: 将output的定义放在方法中。
- 方法二:在调用方法的时候采用不同的对象。
- 我采用的上述的方法二,对测试代码进行了如下的修改:
ShuntingYard shuntingYard = new ShuntingYard(); ShuntingYard shuntingYard1 = new ShuntingYard(); ShuntingYard shuntingYard2= new ShuntingYard(); public void testGetEquation() {//测试中缀转后缀 assertEquals("6 65 79 62 ÷ - * ",shuntingYard.getEquation("6 * ( 65 - 79 ÷ 62 )")); assertEquals("63 0/5 2 5 26 32 + + - ÷ + ",shuntingYard1.getEquation("63 + 0/5 ÷ ( 2 - ( 5 + ( 26 + 32 ) ) )")); assertEquals("65 5/7 - 40 - 48 * 23 - 52 33 12 * 87 ÷ 87 ÷ - + ", shuntingYard2.getEquation("( 65 - 5/7 - 40 ) * 48 - 23 + ( 52 - ( 33 * 12 ÷ 87 ) ÷ 87 )")); }
- 测试结果截图:
九、结对感受
- 不行了,我真的来不及了,天天做题做到自闭,回头一看作业还没写。但是结对只有两个人,我要是放弃了,肯定做不了,所以还是要一直做。
- 我只负责了代码实现,junit测试全是对方来弄,然后我再pull下来跟着学,然后看看看有什么可以改进的地方修修补补。
- 这次结对我感觉是我拖后腿了,有点不好意思。之后有机会再慢慢补吧。
- 我真的现在实力很弱,一个简单的bug要调好久,IDEA还经常出问题,浪费了很多时间,其实可以做的更好。
- 队友很棒,而且莫名其妙的谦虚,明明她做的很好很多,就是编程习惯不太好,目录也有点乱。我虽然笨,但是有强迫症,整整代码看起来还蛮清爽。总之这次结对确实是一个考验,我们都从对方身上学到了很多东西。
十、PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(小时/分钟) | 实际耗时(小时/分钟) |
---|---|---|---|
Planning | 计划 | 2小时 | 4小时 |
· Estimate | · 估计这个任务需要多少时间 | 20小时 | 28小时半 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 4小时 | 2小时半 |
· Design Spec | · 生成设计文档 | ||
· Design Review | ·设计复审(和同事审核设计文档) | 2小时 | 1小时 |
·Code Standard | ·代码规范 | 半小时 | 50分钟 |
·Design | ·具体设计 | 1小时 | 半小时 |
·Coding | ·具体编码 | 5小时 | 11小时 |
·Code Review | ·代码复审 | 2小时 | 8小时 |
·Test | ·测试(自我测试,修改代码,提交修改) | 1小时 | 6小时半 |
Reporting | 报告 | 1小时 | 3小时 |
·Test Report | ·测试报告 | ||
·Size Measurement | ·计算工作量 | 半小时 | 半小时 |
·Postmortem&Process Improvement Plan | ·事后总结,并提出过程改进计划 | 半小时 | 40分钟 |
合计 | 19小时半 | 38小时半 |