1.项目Github地址
https://github.com/Vertigor/FourOperation
2.题目
(四则运算题目生成程序(基于控制台)){https://edu.cnblogs.com/campus/whu/2017ASE/homework/952}
3.估计花费时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | |
· Estimate | · 估计这个任务需要多少时间 | 10 | |
Development | 开发 | 360 | |
· Analysis | · 需求分析 (包括学习新技术) | 30 | |
· Design Spec | · 生成设计文档 | 20 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | |
· Design | · 具体设计 | 30 | |
· Coding | · 具体编码 | 230 | |
· Code Review | · 代码复审 | 20 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 10 | |
Reporting | 报告 | 60 | |
· Test Report | · 测试报告 | 30 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | |
合计 | 430 |
3.
4.解题思路
在拿到题目后,一开始想的是算法的实现,后来感觉算法是一定可以实现的,然而运行在什么样的环境,是至关重要的。本来想选用最近一直在用的JAVA,但是由于我不会JAVA的界面编程,最后还是选择了之前比较熟练的C#。又想到这个项目应该有很好的兼容性,网页似乎就是一个不错的选择。但后来又发现题目要求基于控制台,感觉自己白浪费这么多时间在界面上了。汗。。。最后还是用JAVA吧,电脑上的环境已经搭好了,不用重新搭C#的环境了。
Java是一门面向对象的语言,所以分析题目中的对象是很重要的。首先,要把界面、功能模块、控制分割开,采用MVC模式设计。其次,用户操作的对象是题目,将题目作为对象生成一个类(Question)。题目中要求可以进行真分数的运算,我的想法是把每个数字都当成是真分数来运算,整数相当于分母为1,所以我又生成一个类(Fractions)。光有对象没有操作不行的,我又声明一个类(Calculate),用来计算真分数的加减乘除。最后,生成一个(Control)类,用来处理与用户交互的图形界面,这样程序的框架就搭好了。
完成框架后,具体设计还需满足以下需求:
- 操作数必须随机生成。
- 运算符的种类和顺序必须随机生成。
- 可以判断用户输入的对错。
- 使用-n参数控制生成题目的个数。
- 支持带括号的多元复合运算。
- 运算符个数随机生成。
补充:考虑真分数可以约分成整数,所以Fractions中应该包含changeToInteger()函数,将结果保存成整数。括号的数目和位置也应该随机生成,满足数学约束,并且能够嵌套。
5.设计实现过程
总流程图
子流程图
Fractions用两个int变量分别表示分子分母,提供静态函数maxCommonDivisor(int,int)和minCommonMultiple(int, int),分别是求最大公约数函数和最小公倍数函数,还包含将可转化为整数的分数转化为整数的函数changeToInteger()。
Question采用两种数组保存操作数,分别是分数操作数和整数操作数,又创建两个括号数组,分别是左括号和右括号,专门的乘除运算符数组以及用于计算的两个堆栈。包括检查括号约束情况函数checkBracket()、计算函数calculate()、优先级比较函数compare(str: char)等。
Calculate包含四个静态函数,分别是加减乘除。Control包含main函数,从控制台读取到题目个数,与用户进行交互。
6.代码说明
代码说明已存在注释中。
6.1Question类如下:
1 package question; 2 3 import java.util.Random; 4 import java.util.Stack; 5 6 import fraction.Fractions; 7 import calculate.Calculate; 8 9 public class Question { 10 private Character[] operators;//操作符数组 11 private int[] operands;//操作数数组 12 private Fractions[] operands_fra;//操作数分数数组 13 private int operators_num;//运算符数目 14 private Fractions result;//计算结果 15 private Stack<Character> priStack;// 操作符栈 16 private Stack<Fractions> numStack;// 操作数栈 17 private int[] leftBracket;//左括号 18 private int[] rightBracket;//右括号 19 private int bracketNum;//括号数量 20 private String expression;//表达式字符串 21 public Question(int operators_num){ 22 if(operators_num<1||operators_num>10){ 23 System.out.println("Error:operators number error!"); 24 return; 25 } 26 this.operators_num = operators_num; 27 this.operands = new int[operators_num+1]; 28 this.operators = new Character[operators_num]; 29 this.operands_fra = new Fractions[operators_num+1]; 30 this.init(); 31 } 32 //初始化各种参数 33 private void init(){ 34 Random random=new Random(); 35 if(operators_num==1) 36 bracketNum=0; 37 else 38 bracketNum=random.nextInt(operators_num/2+operators_num%2+1); 39 leftBracket = new int[operators_num]; 40 rightBracket = new int[operators_num]; 41 priStack = new Stack<Character>(); 42 numStack = new Stack<Fractions>(); 43 initBracketArray(); 44 if(bracketNum>0){ 45 for(int i=0;i<this.bracketNum;i++){ 46 int pos = random.nextInt(operators_num); 47 leftBracket[pos]++; 48 rightBracket[random.nextInt(operators_num-pos)+pos]++; 49 50 } 51 checkBracket(); 52 } 53 for(int i=0;i<this.operands.length;i++){ 54 operands[i]=random.nextInt(100)+1; 55 } 56 for(int i=0;i<this.operands_fra.length;i++){ 57 operands_fra[i]=new Fractions(operands[i],1); 58 } 59 for(int i=0;i<this.operators.length;i++){ 60 switch(random.nextInt(4)){ 61 case 0: 62 operators[i]='+'; 63 break; 64 case 1: 65 operators[i]='-'; 66 break; 67 case 2: 68 operators[i]='*'; 69 break; 70 case 3: 71 operators[i]='/'; 72 break; 73 } 74 } 75 this.setExpression(printQuestion()); 76 this.calculate(); 77 } 78 //初始化括号数组 79 private void initBracketArray(){ 80 for(int i=0;i<this.operators_num;i++){ 81 leftBracket[i]=0; 82 rightBracket[i]=0; 83 } 84 } 85 //检查括号是否满足约束,不满足删除括号 86 private boolean checkBracket(){ 87 boolean flag = true; 88 int[] lb = leftBracket.clone(); 89 int[] rb = rightBracket.clone(); 90 for(int i=0;i<operators_num;i++){ 91 int temp =i; 92 while(rb[i]>0){ 93 for(int j=i;j>-1;j--){ 94 while(lb[j]>0&&rb[i]>0){ 95 lb[j]--; 96 rb[i]--; 97 if(temp-1==j||temp==j||(i==operators_num-1&&j==0)){ 98 deleteBracket(j, i); 99 flag = false; 100 } 101 temp=j; 102 } 103 } 104 } 105 } 106 return flag; 107 } 108 //删除括号 109 private boolean deleteBracket(int lb,int rb){ 110 if(leftBracket[lb]==0||rightBracket[rb]==0) 111 return false; 112 leftBracket[lb]--; 113 rightBracket[rb]--; 114 bracketNum--; 115 return true; 116 } 117 //打印表达式字符串 118 private String printQuestion(){ 119 String str=""; 120 for(int i=0;i<operators_num;i++){ 121 for(int j=0;j<leftBracket[i];j++){ 122 str+="("; 123 } 124 str+=operands[i]; 125 if(i>0){ 126 for(int j=0;j<rightBracket[i-1];j++){ 127 str+=")"; 128 } 129 } 130 str+=operators[i].toString(); 131 } 132 str+=operands[operators_num]; 133 if(bracketNum>0) 134 for(int j=0;j<rightBracket[operators_num-1];j++){ 135 str+=")"; 136 } 137 str+="="; 138 return str; 139 } 140 //计算表达式 141 private void calculate(){ 142 numStack.push(operands_fra[0]); 143 int i=0; 144 int[] lb = leftBracket.clone(); 145 int[] rb = rightBracket.clone(); 146 while(i<operators_num){ 147 while(lb[i]>0){ 148 priStack.push('('); 149 lb[i]--; 150 } 151 if(i>0){ 152 if(rb[i-1]>0){ 153 char ope = priStack.pop(); 154 if(ope=='(') 155 continue; 156 Fractions b = (Fractions) numStack.pop();// 第二个运算数 157 Fractions a = (Fractions) numStack.pop();// 第二个运算数 158 Fractions tempresult ; 159 switch (ope) { 160 // 如果是加号或者减号,则 161 case '+': 162 tempresult = Calculate.addtion(a, b); 163 numStack.push(tempresult); 164 break; 165 case '-': 166 tempresult = Calculate.subtraction(a, b); 167 numStack.push(tempresult); 168 break; 169 case '*': 170 tempresult = Calculate.multiplication(a, b); 171 numStack.push(tempresult); 172 break; 173 case '/': 174 tempresult = Calculate.division(a, b); 175 numStack.push(tempresult); 176 break; 177 } 178 rb[i-1]--; 179 } 180 } 181 if(!compare(operators[i])){ 182 Fractions b = (Fractions) numStack.pop();// 第二个运算数 183 Fractions a = (Fractions) numStack.pop();// 第二个运算数 184 char ope = priStack.pop(); 185 Fractions tempresult ; 186 switch (ope) { 187 // 如果是加号或者减号,则 188 case '+': 189 tempresult = Calculate.addtion(a, b); 190 numStack.push(tempresult); 191 break; 192 case '-': 193 tempresult = Calculate.subtraction(a, b); 194 numStack.push(tempresult); 195 break; 196 case '*': 197 tempresult = Calculate.multiplication(a, b); 198 numStack.push(tempresult); 199 break; 200 case '/': 201 tempresult = Calculate.division(a, b); 202 numStack.push(tempresult); 203 break; 204 } 205 }else{ 206 priStack.push(operators[i]); 207 numStack.push(operands_fra[i+1]); 208 i++; 209 } 210 } 211 while(!priStack.isEmpty()){ 212 char ope = priStack.pop(); 213 if(ope=='(') 214 continue; 215 Fractions b = (Fractions) numStack.pop();// 第二个运算数 216 Fractions a = (Fractions) numStack.pop();// 第一个运算数 217 Fractions tempresult ; 218 switch (ope) { 219 // 如果是加号或者减号,则 220 case '+': 221 tempresult = Calculate.addtion(a, b); 222 numStack.push(tempresult); 223 break; 224 case '-': 225 tempresult = Calculate.subtraction(a, b); 226 numStack.push(tempresult); 227 break; 228 case '*': 229 tempresult = Calculate.multiplication(a, b); 230 numStack.push(tempresult); 231 break; 232 case '/': 233 tempresult = Calculate.division(a, b); 234 numStack.push(tempresult); 235 break; 236 } 237 } 238 239 result = numStack.pop(); 240 } 241 private boolean compare(char str) { 242 if (priStack.empty()) { 243 // 当为空时,显然 当前优先级最低,返回高 244 return true; 245 } 246 char last = (char) priStack.lastElement(); 247 // 如果栈顶为'('显然,优先级最低,')'不可能为栈顶。 248 if (last == '(') { 249 return true; 250 } 251 switch (str) { 252 case '=': 253 return false;// 结束符 254 case '(': 255 // '('优先级最高,显然返回true 256 return true; 257 case ')': 258 // ')'优先级最低, 259 return false; 260 case '*': { 261 // '*/'优先级只比'+-'高 262 if (last == '+' || last == '-') 263 return true; 264 else 265 return false; 266 } 267 case '/': { 268 if (last == '+' || last == '-') 269 return true; 270 else 271 return false; 272 } 273 // '+-'为最低,一直返回false 274 case '+': 275 return false; 276 case '-': 277 return false; 278 } 279 return true; 280 } 281 public Fractions getResult() { 282 return result; 283 } 284 public String getExpression() { 285 return expression; 286 } 287 private void setExpression(String expression) { 288 this.expression = expression; 289 } 290 291 }
6.2Calculate类如下:
1 package calculate; 2 3 import fraction.Fractions; 4 public class Calculate { 5 6 public Calculate(){ 7 } 8 // 加法计算 9 public static Fractions addtion(Fractions fractions1,Fractions fractions2) 10 { 11 int result_numerator,min; // 相加后的分子以及两分数分母的最小公倍数 12 min=Fractions.minCommonMultiple(fractions1.getDenominator(), fractions2.getDenominator()); 13 result_numerator=(min/fractions1.getDenominator())*fractions1.getNumerator()+(min/fractions2.getDenominator())*fractions2.getNumerator(); 14 Fractions result=new Fractions(result_numerator, min); 15 return result; 16 } 17 // 减法计算 18 public static Fractions subtraction(Fractions fractions1,Fractions fractions2) 19 { 20 int result_numerator,min; // 相减后的分子以及两分数分母的最小公倍数 21 min=Fractions.minCommonMultiple(fractions1.getDenominator(), fractions2.getDenominator()); 22 result_numerator=(min/fractions1.getDenominator())*fractions1.getNumerator()-(min/fractions2.getDenominator())*fractions2.getNumerator(); 23 Fractions result=new Fractions(result_numerator, min); 24 return result; 25 } 26 // 乘法计算 27 public static Fractions multiplication(Fractions fractions1,Fractions fractions2) 28 { 29 int result_numerator,result_denominator; // 相乘后的分子和分母 30 result_numerator=fractions1.getNumerator()*fractions2.getNumerator(); 31 result_denominator=fractions1.getDenominator()*fractions2.getDenominator(); 32 Fractions result=new Fractions(result_numerator, result_denominator); 33 return result; 34 } 35 // 除法计算 36 public static Fractions division(Fractions fractions1,Fractions fractions2) 37 { 38 int result_numerator,result_denominator; // 相除后的分子和分母 39 // 分数相除问题转换成分数相乘问题 40 result_numerator=fractions1.getNumerator()*fractions2.getDenominator(); 41 result_denominator=fractions1.getDenominator()*fractions2.getNumerator(); 42 Fractions result=new Fractions(result_numerator, result_denominator); 43 return result; 44 } 45 }
6.3Control类如下:
package control; import java.util.Random; import java.util.Scanner; import fraction.Fractions; import question.Question; public class Control { public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("本次共有"+args[1]+"道题。"); if(args[0].equals("-n")){ Scanner scanner=new Scanner(System.in); Integer num=new Integer(args[1]); int correct=0; String answer[]=new String[num];//用户输入的答案 Random random = new Random(); boolean judge[] = new boolean[num];//保存用户输入对错 Question[] questions = new Question[num]; for(int i=0;i<num;i++){ questions[i] = new Question(random.nextInt(10)+1); System.out.print((i+1)+":"+questions[i].getExpression()); answer[i] = scanner.nextLine(); Fractions result = questions[i].getResult(); int result_int = result.changeToInteger(); if(!answer[i].equals("")){ if(result_int!=Integer.MAX_VALUE){ if(Integer.parseInt(answer[i])==result_int){ judge[i]=true; System.out.println("正确!"); correct++; }else{ judge[i]=false; System.out.println("不正确!正确答案:"+result_int); } }else{ String splits[] = answer[i].split("/"); if(splits.length==2&&Integer.parseInt(splits[0])==result.getNumerator()&&Integer.parseInt(splits[1])==result.getDenominator()){ judge[i]=true; System.out.println("正确!"); correct++; }else{ judge[i]=false; System.out.println("不正确!正确答案:"+result.printFraction()); } } }else{ judge[i]=false; System.out.println("未回答!正确答案:"+result.printFraction()); } } double score = (double)correct/(double)num*100.00; System.out.println("本次得分:"+score); scanner.close(); }else{ System.out.println("命令有误"); } } }
6.4Fractions类如下:
1 package fraction; 2 3 public class Fractions { 4 private int numerator; //分子 5 private int denominator; //分母 6 // 无参数构造器 7 public Fractions(){ 8 } 9 //参数构造器 10 public Fractions(int numerator,int denominator){ 11 this.setValue(numerator, denominator); 12 } 13 // 设置分子分母 14 public void setValue(int numerator,int denominator) 15 { 16 if(numerator==0){ 17 this.numerator=0; 18 this.denominator=1; 19 return; 20 } 21 if(denominator==0){ 22 System.out.println("Error:denominator equals zero!"); 23 } 24 int temp=maxCommonDivisor(denominator, numerator); //temp为最大公约数 25 this.numerator=numerator/temp; 26 this.denominator=denominator/temp; 27 } 28 // 求最大公约数 29 public static int maxCommonDivisor(int d, int n) 30 { 31 if (d < n) {// 保证d>n,若d<n,则进行数据交换 32 int temp = d; 33 d = n; 34 n = temp; 35 } 36 while (d % n != 0) {// 在余数不能为0时,进行循环 37 int temp = d % n; 38 d = n; 39 n = temp; 40 } 41 return n;// 返回最大公约数 42 } 43 // 求最小公倍数 44 public static int minCommonMultiple(int m, int n) { 45 return m * n / maxCommonDivisor(m, n); 46 } 47 // 打印分数 48 public String printFraction() 49 { 50 return (this.numerator+"/"+this.denominator).toString(); 51 } 52 // 获取分子 53 public int getNumerator() 54 { 55 return this.numerator; 56 } 57 // 获取分母 58 public int getDenominator() 59 { 60 return this.denominator; 61 } 62 //判断是否可以转化为整数 63 private boolean isInteger(){ 64 if(this.denominator==1||this.denominator==-1) 65 return true; 66 else return false; 67 } 68 //转换为整数 69 public int changeToInteger(){ 70 if(this.isInteger()) 71 return this.getNumerator(); 72 else 73 return Integer.MAX_VALUE; 74 } 75 }
7.测试运行
7.1程序测试
程序截图如下:
由上图可知,程序界面显示没问题,满足需求,括号也满足了嵌套,数学约束,结果由人工手算也是正确的。当然,这张图只是程序跑一次的结果,这个程序我跑了50次左右,结果都是正确(其中出BUG的部分也被修补过了)。在运行中,一开始结果是不正确的,错误集中在calculate()函数中,栈的入栈出栈顺序有问题,编程的时候没发现,发现错误的时候很难找出来。在括号数学约束中,bug也是很多的,会出现(34)+23这样的表达式,这段代码我看了很多遍,一直都没发现错误,后来才发现是少减了1,(⊙﹏⊙)b这个bug我纠结了2个小时。
7.2单元测试
7.2.1Calculate功能测试
先展示单元测试的结果图:
在此次单元测试中,分别测试了Calculate类中加减乘除的运算,例如:测试加法函数,输入(1/2+1/2),对比结果是否为1。具体测试类如下:
1 package calculate; 2 3 import static org.junit.Assert.*; 4 5 import org.junit.Before; 6 import org.junit.Test; 7 8 import fraction.Fractions; 9 10 public class CalculateTest { 11 12 @Before 13 public void setUp() throws Exception { 14 } 15 16 @Test 17 public void testAddtion() { 18 Fractions result = Calculate.addtion(new Fractions(1,2), new Fractions(1,2)); 19 assertEquals(1, result.getNumerator()); 20 assertEquals(1, result.getDenominator()); 21 } 22 23 @Test 24 public void testSubtraction() { 25 Fractions result = Calculate.subtraction(new Fractions(1,2), new Fractions(1,2)); 26 assertEquals(0, result.getNumerator()); 27 assertEquals(1, result.getDenominator()); 28 } 29 30 @Test 31 public void testMultiplication() { 32 Fractions result = Calculate.multiplication(new Fractions(1,2), new Fractions(1,2)); 33 assertEquals(1, result.getNumerator()); 34 assertEquals(4, result.getDenominator()); 35 } 36 37 @Test 38 public void testDivision() { 39 Fractions result = Calculate.division(new Fractions(1,2), new Fractions(1,2)); 40 assertEquals(1, result.getNumerator()); 41 assertEquals(1, result.getDenominator()); 42 } 43 44 }
7.2.2Fractions功能测试
Fractions类的测试结果如图:
在Fractions中主要测试的是最大公约数,最小公倍数函数,分数转整数,打印分数,设置分数值。代码如下:
1 package fraction; 2 3 import static org.junit.Assert.*; 4 5 import org.junit.Before; 6 import org.junit.Test; 7 8 public class FractionsTest { 9 10 private Fractions fraction; 11 12 @Before 13 public void setUp() throws Exception { 14 fraction = new Fractions(1,1); 15 } 16 17 @Test 18 public void testSetValue() { 19 fraction.setValue(2, 3); 20 assertEquals(2, fraction.getNumerator()); 21 assertEquals(3, fraction.getDenominator()); 22 } 23 24 @Test 25 public void testMaxCommonDivisor() { 26 int divisor = fraction.maxCommonDivisor(9, 6); 27 assertEquals(3, divisor); 28 } 29 30 @Test 31 public void testMinCommonMultiple() { 32 int multiple = fraction.minCommonMultiple(9, 6); 33 assertEquals(18, multiple); 34 } 35 36 @Test 37 public void testPrintFraction() { 38 assertEquals("1/1", fraction.printFraction()); 39 } 40 41 @Test 42 public void testChangeToInteger() { 43 assertEquals(1, fraction.changeToInteger()); 44 } 45 46 }
7.3代码覆盖测试
代码覆盖测试使用的是EclEmma插件,运行结果截图如下:
程序覆盖率在82%,其中有一些是判断失败的语句以及一些表达式并没有生成括号,所以一些代码没有运行到。
8.实际花费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 30 |
Development | 开发 | 360 | 600 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 30 |
· Design Spec | · 生成设计文档 | 20 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 230 | 420 |
· Code Review | · 代码复审 | 20 | 10 |
· Test | · 测试(自我测试,修改代码,提交修改) | 10 | 30 |
Reporting | 报告 | 60 | 360 |
· Test Report | · 测试报告 | 30 | 330 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 430 | 990 |
9.项目小结
此次项目帮助我深入了解了PSP的工作流程,从计划、开发到测试,每一步都亲身体验,从中学到了许多,掌握了很多Eclipse的插件,比如:Eclipse自带的Git插件、GEF插件、单元测试等。我深刻体会到软件工程的系统性和复杂性,正如:
软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。它涉及程序设计语言、数据库、软件开发工具、系统平台、标准、设计模式等方面。在现代社会中,软件应用于多个方面。典型的软件有电子邮件、嵌入式系统、人机界面、办公套件、操作系统、编译器、数据库、游戏等。同时,各个行业几乎都有计算机软件的应用,如工业、农业、银行、航空、政府部门等。这些应用促进了经济和社会的发展,也提高了工作效率和生活效率 。
由于一开始没有单元测试的概念,导致单元测试都是在完成整个软件的时候才开始的。单元测试应该在完成每一个功能模块的时候就进行,这样才能保证每一个功能的正确性。并且在更改和更新的时候还需要做回归测试,保证原有功能正常,不受新功能的影响。