结对开发照片:
本次需要完成的任务其实我在上次的任务中已经完成得差不多了,实现分数的计算,括号的插入和指定操作数个数都已经完成。需要添加的功能主要是实现将真分数表示为带分数,比较表达式是否相同和将数据插入数据库。
项目开发前的预估如下表,由于没有实际开发经验,只能根据书上的提示和老师的指导大致估测。
PSP2.1 | Personal Software Process Stages | Time |
Planning | 计划 | 20h |
· Estimate | · 估计这个任务需要多少时间 | 20h |
Development | 开发 | 15h |
· Analysis | · 需求分析 (包括学习新技术) | 2h |
· Design Spec | · 生成设计文档 | 1h |
· Design Review | · 设计复审 (和同事审核设计文档) | 1h |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 1h |
· Design | · 具体设计 | 2h |
· Coding | · 具体编码 | 4h |
· Code Review | · 代码复审 | 1h |
· Test | · 测试(自我测试,修改代码,提交修改) | 3h |
Reporting | 报告 | 5h |
· Test Report | · 测试报告 | 2 |
· Size Measurement | · 计算工作量 | 1.5h |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 1.5h |
| 合计 | 20h |
具体到开发过程中,我光分析需求设计算法就花了将近5个小时的时间,这还只是保守的估计,因为有些思考时间比较零散,无法计算。然后编程花了将近12个小时。这比我估计的要多得多,只不过我是编程过程中就在测试,测试和编程混在一起了,两则时间无法计算了。在记录过程中我发现一旦开始编程,就会全心投入,总是忘了记录时间、测试用例以及程序缺陷,而且压根儿就不会按照PSP规范走。所以最后得到的记录表格和实际还是有些出入的。
PSP2.1 | Personal Software Process Stages | Time |
Planning | 计划 |
|
· Estimate | · 估计这个任务需要多少时间 |
|
Development | 开发 | 18h |
· Analysis | · 需求分析 (包括学习新技术) | 6 |
· Design Spec | · 生成设计文档 | 没有 |
· Design Review | · 设计复审 (和同事审核设计文档) | 没有 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 一般的java编程规范 |
· Design | · 具体设计 | 2h |
· Coding | · 具体编码 | 10h |
· Code Review | · 代码复审 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 和编码一块儿做了 |
Reporting | 报告 | 2h |
· Test Report | · 测试报告 | 2h |
· Size Measurement | · 计算工作量 |
|
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 |
|
| 合计 | 20h |
程序设计思路
首先要考虑的问题是程序中生成的表达式应该存放在什么地方。就我看来不外乎两种方式,数组和字符串。我选择了字符串,因为在我看来,如果用数组存放不方便括号的处理(也许其他同学有办法,而且觉得数组好)。然后,因为如果有除法运算,结果需要表示为真分数或者带分数形式的字符串,所以在运算过程中必定会牵涉到有分数参与的计算。为了解决这个问题,需要用到分数类,这个类java中似乎没有提供(也许提供了我不知道),所以我自己编写了一个分数类,这个类需要拥有分数的加减乘除功能、比较大小功能等等。其实这一步我在四则运算二的任务重就已经完成了,只是有一点需要重新考虑,在表达式中假分数需要表示为带分数形式,这只需要重新编写toString()方法就行了。另外一个小问题(对我来说也是一个关键问题,解决这个问题候就解决了问题的一半),如何分辨表达式中的除号和分数的分数线。可能有人会说分数线和除号不都是一个意义吗?是的,分数线和除法符号在数学上就是一个意思,但是如果表达式中出现两个连续的除号(除号都用‘/’)例如a/b/c,那么究竟是a除以b除以c呢,还是a除以c分之b呢?老师要求在运算符和操作数之间留有空格,用来区分以上情况,但是这样求值会很困难,因为我是先构建表达式存放在字符串中,然后再求值。及反思所我还是想到了一个好办法 ,用标准的四则运算符号表示运算(﹢﹣×÷)表示即可,反正这只是在字符串中的一个表现形式,不需要程序识别这些运算符。至于在程序中怎么输入这些符号,其实也简单,搜狗输入法可以输入一些特殊的字符。然后是程序的主体部分,求值。其实表达式求值没有什么难的,就是应用堆栈将中缀表达式化为后缀表达式,这些在数据结构中都学过。最后一个难题,怎么比较两个表达式是否重复。我想了很久还是想出来一个方法,这里我把算法列出来。设l1,l2是两个表达式。
- 如果l1,l2的值不相同,程序结束,返回false。
- 如果l1,l2的字符串相同,及表达式为一模一样,程序结束,返回true
- 如果l1,l2的值不相同且字符串不相同,这时我们要判断是否可以经过有限次加法或乘法交换得到相同的表达式。执行以下步骤:
(1) 如果l1,l2的操作数个数不相同,返回false,程序结束。
(2) 如果l1,l2的操作数个数相同且都为2,我们便可以方便的判断出这两个表达式是否相同。
(3) 如果l1,l2的操作数个数相同而且不为2,执行以下步骤:
1) 取l1,l2中最先运算的子表达式e1,e2.
2) 递归调用本算法比较e1,e2是否相同。
3) 如果e1,e2不相同,返回false,程序结束。
4) 如果e1,e2不相同,用e1,e2的结果替代原表达式中的e1,e2得到两个新的表达式n1,n2。递归调用本方法比较n1,n2是否相同,相同返回true,不相同返回false。
上面这个算法步骤比较复杂,其中还用到一些其他的步骤,比如如何取出表达式中最先运算的子表达式,如果计算表达式中操作数的个数等。这些都不是简单事。
实验代码:
import java.util.ArrayList;import java.util.Scanner;//import java.util.Scanner;public class Equation { public static void main(String[] args) throws Exception { // TODO Auto-generated method stub Scanner scan = new Scanner(System.in); boolean flag = false; boolean ismul = false; boolean withbrackets = false; int num = 0; int sum = 0; int zhen = 0; int fenzi = 0; int fenmu = 0; do{ try{ System.out.println("您需要多少个式子:"); num = scan.nextInt(); System.out.println("是否含有乘除法?(true/false)"); ismul = scan.nextBoolean(); System.out.println("操作数的个数:"); sum = scan.nextInt(); System.out.println("整数最大值:"); zhen = scan.nextInt(); System.out.println("分子最大值:"); fenzi = scan.nextInt(); System.out.println("分母最大值:"); fenmu = scan.nextInt(); System.out.println("是否包含括号:(true/false)"); withbrackets = scan.nextBoolean();scan.nextLine(); flag = false; int[] correct = new int[num]; int[] wrong = new int[num]; int correctsum = 0; int wrongsum = 0; for(int index = 0;index0) { System.out.print(" 正确"+correctsum+" correct("); for(int j < correctsum; j++){ system.out.print(correct[j]); if(j!="correctsum-1)" system.out.print(","); else system.out.println(")"); } if(wrongsum> 0) { System.out.print("错误"+wrongsum+" wrong("); for(int j = 0; j < wrongsum; j++){ System.out.print(wrong[j]); if(j!=wrongsum-1) System.out.print(","); else System.out.println(")"); } } }catch(Exception e){ System.out.println("输入错误,请重新输入!"); flag = true; } }while(flag); scan.close(); } private String express,//表达式 value;//表达式结果 private int number;//操作数个数 public Equation(boolean incmul,int much,int zhen,int fenzi,int fenmu,boolean withbrackets) { //incmul是否包含乘除法,much表示操作数的个数 if(much < 2) much = 2; number = much; boolean tag = false; do{ try { express = createExpress(incmul,much,zhen,fenzi,fenmu,withbrackets); value = evaluation(express); tag = false; } catch (Exception e) { tag = true; } }while(tag); } public Equation(){ this(true,4,100,10,11,true); } public Equation(String exp){ express = exp; try { this.value = evaluation(exp); this.number= this.allFigure().length; } catch (Exception e) { this.express = null; this.value = null; } } //setter and get public String getExpress(){ return express;} public void setExpress(String ex){express = ex;} public String getValue(){ return value;} public int getNumbers(){ return number;} public void setNumber(int n){number = n;} public String toString(){ return express+" = "+value; } public boolean equals(Equation another)//比较两个算式是否相同 { if(this.express==null||this.value==null||another.express==null||another.value==null) return false; //如果两个等式的表达式相同 if(this.express.equals(another.express)) return true; //如果两个表达式的值不相同,则程序结束 if(!getValue().equals(another.getValue())) return false; else{ //将表达式分解成数字串和操作符,TODO 如果表达式中没有空格将不能分开 String[] part1 = this.separateAll(); String[] part2 = another.separateAll(); int len1 = part1.length; int len2 = part2.length; //如果分开后的数组长度小于3,则程序出现异常,返回false if(len1<3||len2<3) return false; //如果两个数组长度不相等,则原表达式不相等 if(len1!=len2) return false; //否则 else{ //如果表达式包含一个操作符和两个操作数 if(len1==3){ //如果两个表达式的操作数对应相同 if((part1[0].equals(part2[0])&&part1[2].equals(part2[2]))) { if(part1[1].equals(part2[1])) return true; else return false; } //如果两个表达式的操作数交叉相同 else if(part1[0].equals(part2[2])&&part1[2].equals(part2[0])) { //如果两个表达式的操作符不相同 if(!part1[1].equals(part2[1])) return false; //操作符相同 else{ //如果是加法或者乘法 if(part1[1].equals("﹢")||part1[1].equals("×")) return true; //如果是减法或者除法 else return false; } } //如果表达式的操作数不相同 else return false; } //如果表达式操作符个数大于一且操作数大于二 else{ //取两个表达式中优先级最高的子表达式 Equation e1 = this.precede(); Equation e2 = another.precede(); //比较两个子表达式是否相同 boolean tag = e1.equals(e2); //如果子表达式不相同 if(!tag) return tag; //如果子表达式相同 else{ //用子表达式的值取代子表达式在原式中的位置,构成新的两个表达式。 String newstr1 = this.express.replaceFirst(e1.express, e1.value); newstr1 = removeBracket(newstr1,e1.value);//处理一个数由一对括号包围的情况 String newstr2 = another.express.replaceFirst(e2.express, e2.value); newstr2 = removeBracket(newstr2,e1.value);//处理一个数由一对括号包围的情况 e1 = new Equation(newstr1); e2 = new Equation(newstr2); //比较两个新的表达式是否相同,且返回比较结果。 return e1.equals(e2); } } } } } /** * 2017/3/19日增加removeBracket()方法 * 在去优先级最高的子表达式时,有可能子表达式被一对括号包含,但通过precede * 方法取得的子表达式不含有括号。在equals方法中,用子表达式的值取代子表达式后 * 有可能出现一个数带有一个括号,该方法就是去掉这种情况 * */ private static String removeBracket(String orign,String subvalue)//去掉包含单个数字的括号。 { //orign 是原来的表达式字符串形式被自表达式的值取代后的字符串,subvalue为子表达式的值 String str = new String(orign); String regex = "("+subvalue+")"; if(orign.indexOf(regex)>=0) str = orign.replace(regex, subvalue); return str; } private static String getFraction(int x,int y,int z)//生成一个分数 { if(z == 0) return "0"; double f = Math.random();//随机数 Fraction fr = null;//声明一个分数 if(f < 0.3){ //如果随机数的大于0.3生成一个分数 //随机数c表示分子 int c = (int)(Math.random()*y); //随机数d表示分母,注意分母不能为零 int d = (int)(Math.random()*(z-1))+1; try { fr = new Fraction(c,d); return fr.toString(); } catch (Exception e) { e.printStackTrace(); } }else{ //否则生成一个整数 int c = (int)(Math.random()*x); return ""+c; } return null; } private static String createExpress(boolean incmul,int num,int zhen,int fenzi,int fenmu,boolean withbrackets) //生成一个表达式,incmul表示是否支持乘除法,num表示操作数的个数 { if(num<2) num = 2; char[] operator = {'﹢','﹣','×','÷'};//存放操作符的字符数组 if(!incmul)//如果不支持乘除法,将'×','÷'替换为'﹢','﹣' { operator[2] = '﹢'; operator[3] = '﹣'; } String[] strArray = new String[num]; char[] opeArray = new char[num-1]; for(int i = 0; i < num; i++) { strArray[i] = getFraction(zhen,fenzi,fenmu); if(i < num -1){ int index = (int)(Math.random()*4); opeArray[i] = operator[index]; } } if(withbrackets) insertBrackets(strArray,0,num-1); String str = ""; for(int i = 0; i < num; i++) { str += strArray[i]; if(i < num-1) str += " "+opeArray[i]+" "; } return str; } private static void insertBrackets(String[] exp,int index_1,int index_2) { /** *2017/3/15日修改插入括号的方法,先生成操作数,然后将括号加到操作数中 */ int y = index_2 - index_1; if(y < 2) return ; int i = (int)(Math.random()*(y))+index_1; int j = (int)(Math.random()*(y+1))+index_1; if(i>j){ i = i^j; j = i^j; i = i^j; } int x = j - i; int z = exp.length-1; if(x >= 1&&x!=z&&x!=y){ exp[i] = "("+exp[i]; exp[j] = exp[j]+")"; } insertBrackets(exp,index_1,i-1); insertBrackets(exp,i,j); insertBrackets(exp,j+1,index_2); } private static String evaluation(String express)throws Exception//计算表达式的结果 { //构建两个栈,分别存放分数和操作符 java.util.Stack mstack = new java.util.Stack (); java.util.Stack ostack=new java.util.Stack (); boolean flag = false;//旗帜变量表示上一个分数是否压入了mStack栈中 express += "#"; int i = 0,len = express.length(); ostack.push('#'); String str = "";//中间字符串变量,用来读取表达式中的一个分数或者整数 while(i < len){ char ch = express.charAt(i); //当前字符为一个分数或者整数的一部分。 if((ch>='0' && ch<='9') || ch=='/' || ch=='\''){ str += ch; flag = true;//数没有压入栈中 i++; }else{ //如果当前字符是空格或者操作符 if(flag){ //如果上一个操作数还没有压入mStack栈,怎将其压入栈中 Fraction f = new Fraction(str); mstack.push(f); str = ""; flag = false;//数字已经压入栈中 } if(ch == ' ')//如果当前字符是空格则跳过这个字符 i++; else{ //以下为运算过程 char op = ostack.peek(); char tag = compare(op,ch); switch(tag){ case '<': ostack.push(ch);i++;break; case '>': Fraction b = mstack.pop(); Fraction a = mstack.pop(); char oo = ostack.pop(); Fraction c = calcul(a,oo,b); if(c.lessThan(new Fraction(0))) throw new Exception("表达式"+a+oo+b+"的值为负数!"); mstack.push(c);break; case '=': ostack.pop(); i++; break; } } } } Fraction r = mstack.pop(); return r.toString(); } private static Fraction calcul(Fraction a, char oo, Fraction b) throws Exception//a,b按照oo所代表的操作运算 { // TODO Auto-generated method stub if(oo == '﹢') return a.add(b); else if(oo == '﹣') return a.reduce(b); else if(oo == '×') return a.multiply(b); else if(oo == '÷') return a.divide(b); return null; } private static char compare(char s,char t)//比较s,t的优先级 { char tag='<'; //临时变量,用来保存比较结果 switch(s){ case '﹢': if(t == '×' || t == '÷' || t == '(') tag = '<'; else tag = '>'; break; case '﹣': if(t == '×' || t == '÷' || t == '(') tag = '<'; else tag = '>'; break; case '×': if(t == '(') tag = '<'; else tag = '>'; break; case '÷': if(t == '(') tag = '<'; else tag = '>'; break; case '(': if(t == ')') tag = '='; else if(t == '#') break; } return tag; } public String[] separateAll() { String[] s = express.split("[\\s()]+"); return s; } public Fraction[] allFigure() { String ss = express+" "; int len = ss.length(),i=0; boolean flag = false; String str = ""; ArrayList list = new ArrayList (); do{ char ch = ss.charAt(i); //当前字符为一个分数或者整数的一部分。 if((ch>='0' && ch<='9') || ch=='/' || ch=='\''){ str += ch; flag = true;//数没有压入栈中 }else{ if(flag){ try { //System.out.println(str); Fraction f = new Fraction(str); list.add(f); flag = false; str = ""; } catch (Exception e) { e.printStackTrace(); } } } i++; }while(i < len); int size = list.size(); Fraction a[] = new Fraction[size]; for( i = 0; i mstack = new java.util.Stack (); java.util.Stack ostack=new java.util.Stack (); String express =new String( this.express); boolean flag = false;//旗帜变量表示上一个分数是否压入了mStack栈中 express += "#"; int i = 0,len = express.length(); ostack.push('#'); String str = "";//中间字符串变量,用来读取表达式中的一个分数或者整数 while(i < len){ char ch = express.charAt(i); //当前字符为一个分数或者整数的一部分。 if((ch>='0' && ch<='9') || ch=='/' || ch=='\''){ str += ch; flag = true;//数没有压入栈中 i++; }else{ //如果当前字符是空格或者操作符 if(flag){ //如果上一个操作数还没有压入mStack栈,怎将其压入栈中 Fraction f=null; try { f = new Fraction(str); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); f = new Fraction(1); } mstack.push(f); str = ""; flag = false;//数字已经压入栈中 } if(ch == ' ')//如果当前字符是空格则跳过这个字符 i++; else{ //以下为运算过程 char op = ostack.peek(); char tag = compare(op,ch); switch(tag){ case '<': ostack.push(ch);i++;break; case '>': Fraction b = mstack.pop(); Fraction a = mstack.pop(); char oo = ostack.pop(); Fraction c = calcul(a,oo,b); String aa = a.toString(); String bb = b.toString(); String cc = c.toString(); String newstr = aa+" "+oo+" "+bb; Equation newexp = new Equation(newstr); newexp.value = cc; return newexp; case '=': ostack.pop(); i++; break; } } } } }catch(Exception e){e.printStackTrace();} return null; }}
运行结果截图
测试用例
1. 生成一个分数:我写了一个生成分数的方法getFraction(int x,int y,int z),x是限定整数的大小,y用于限定分子的大小,z用来限定分母的大小。下面为测试的截图:
因为无x,y,z的值是何种情况,都可以得到一个可接受的结果,所以这个方法应该没有出错。
2. 测试生成表达式:createExpress()方法用来生成一个表达式,括号中有参数控制表达式的操作数个数,是否含有括号,以及数值范围等。以下是测试截图:
3. 生成有括号的式子:其中并不是每个式子都会带有括号,也不是带括号的式子都只带一对括号,这需要看随机情况。
4. 计算表达式结果:计算结果无误,计算过程中也没有出现负数。
5. 比较两个表达式是否相同:这是最关键的一步,我进行的多册测试,暂时没有发现不和要求的地方。根据题目需求,我认为没有什么疏漏。
6. 算术题是包含分数的,所以实现分数的运算时至关重要的,我写了一个分数类,该类中实现了分数的四则运算,以下测试用是用来测试分数类的。
7. 分数之间的比较测试:分数应该有大小比较,就像整数一样,这里我也给出比较测试的部分截图。
8. 分数除了四则运算,比较大小,还有一中运算,求倒数。但是0是没有倒数的,不能进行求倒数运算。以下是测试用例截图:
9. 分数和小数是可以相互转换的,以下测试为分数转化为小数。
10. 在比较两个算数表达式是否相同是,用到一个私有方法,取表达式中优先级最高的子表达式。这个方法我是自己设计的算法中很重要的一步,这里分享测试用例。
总结:在开发之前,需要有计划有步骤的依次完成作业。开发过程中应该做好记录,不要嫌麻烦,事后要总结经验,写好文档,以备后用。随着代码量的增加,我越来越体会到代码测试的重要性了。在编写开发过程中,无论是谁,总会出现纰漏。往往看似简单的逻辑问题,也会造成最后的结果出人意料。此次编程中,我总是冒进,在各个单元测试的环节中不想花费过多的时间,测试不够全面,代码覆盖率不高而且情况考虑不周,最后在汇总时是不是就会冒出各种七零八落的毛病,又会花费更多的时间来调试。所以今后应当总结经验教训,写代码当循序渐进,稳步前进。