学号20175313 《结对编程四则运算》总结性编程 第七周
一、需求分析
实现一个命令行程序,要求:
自动生成小学四则运算题目(加、减、乘、除)
- 支持整数
- 支持多运算符(比如生成包含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:无法生成嵌套括号、生成的括号位置不正确。形如:
)23+(56
、(23)
这个问题上周我们已经解决了,但是因为我的小伙伴追求精益求精,再加上他本身的实力,后来他用下述代码解决了此问题,确实简洁明了许多。
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--;
}
}
- 问题3:生成的是头尾括号而且头尾括号所包含的内容是式子的全部内容。
- 解决方法:此问题还未能解决。
- 问题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 )")); }
- 测试结果截图:
九、结对感受
- 这是我们一起合作的第二周,我发现他有一个很令人讨厌的地方,就是他每次说话说到一半就不说了,这让好奇心重的我很烦躁。所以,我找他沟通了这个问题。
- 我跟他说你能不能不要每次把话说到一半就不说了,能不能一次性把话说完。他的回答是:我每次说话说到一半就不说了是因为每次我都是突然间脑子里冒出一些奇怪的想法,然后想着想着觉得这个想法不够成熟,所以我就不说了。
- 我语重心长的跟他谈了这个问题,后来他也慢慢的有所改善。
- 有一天,他又欲言又止,我跟他说:有事你说啊,你怎么又这样子(我有点恼火)。后来他为难的说出了他最近要参加比赛,可能最近会比较忙,结对、团队可能无法兼顾。我跟他畅谈了一番,我觉得我们交流的挺好的,这是一个进步。
- 我们两都比较忙,但是我非常幸运他并没有放弃,我们一起熬夜,一起克服困难,总的来说,这周虽然平均睡眠时间减少了两个小时,但是因为有动力,所以依旧精力充沛。
十、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小时半 |