写在前面:
coding.net代码地址: https://git.coding.net/ForeverSevrous/PersonalProject.git
测试效果见src外生成的result.txt
需求分析:
- 接收一个参数n,然后随机产生n道四则运算练习题。
- 每个数字在0和100之间,运算符3个到5个之间。
- 每个练习题至少包含2种运算符。
- 题中不可以出现负数和非整数。
- 支持有括号的运算式,算式中存在的括号必须大于2个,且不得超过运算符的个数。
- 支持真分数的出题与运算,只涵盖加减法。
- 将学号和生成的练习题及对应答案输出到文件“result.txt”中,不输出额外信息。
功能设计:
基本功能:
生成符合题目要求的四则运算题目并输出到文件中。
扩展功能:
随机出现含有正确格式的括号的式子。
生成使用真分数运算的式子。
设计实现:
在实现过程中,我只新建了一个Main类,其他各块功能均由Main类的内部方法实现。
main函数:
主函数,包含接收参数n,判断参数n的合法性,调用函数产生四则运算式,计算运算式结果并输出到文件中的功能。
newE函数:
生成一个符合要求的四则运算式(含有括号)。
Fraction函数:
生成一个符合要求的使用真分数进行加减的四则运算式。
divideExactly函数:
在生成运算式用到“÷”时,判断此时的计算是否能整除,如果不能整除,则返回一个新的可以整除的整数。
calculate函数:
在生成所有四则运算式后,计算所有运算式的结果,并将结果分别附加到每一个四则运算式的尾部。 在计算过程中如果发现某个
式子结果为负数则重新生成一个四则运算式来保证式子的非负性。
write函数:
将calculate类生成的结果写入到已有的文件“result.txt”中。
算法详解:
生成题目:
在生成题目过程中,我先通过随机数确定运算符的个数(3-5个),然后通过循环取随机数确定参与运算的数字(比运算符多一个),实现如下:
1 Random rd = new Random(); 2 //产生运算符的个数,3-5个 3 int on=rd.nextInt(3)+3; 4 //产生比运算符多一个的随机数字 5 int[] nums = new int[on+1]; 6 for(int j=0;j<=on;j++){ 7 nums[j]=rd.nextInt(101); 8 }
之后解决括号相关问题,具体内容见下文【比较满意的代码片段--片段三】
生成真分数运算式的具体内容见下文【比较满意的代码片段--片段四】
计算题目:
在求解题目过程中,为了处理运算符的优先级问题,我使用了Java中的ScriptEngine接口和ScriptEngineManager类,具体代码如下:
1 ScriptEngine se = new ScriptEngineManager().getEngineByName("JavaScript");
使用这个对象需要导入以下三个包:
1 import javax.script.ScriptEngine; 2 import javax.script.ScriptEngineManager; 3 import javax.script.ScriptException;
而且使用这个对象时必须使用异常捕获,否则会报错。使用这个对象的.eval(String),就可以很方便地得出脚本String的值。
测试运行:
控制台输出如下(eclipse):
文件内容如下:
[虽然控制台模式下两种式子分开输出,但是文件中是混合到一起的]
命令行输出如下:
[在这里需要提示一点,使用eclipse时在控制台获取数据一般使用Scanner in = new Scanner(System.in),但是这种方式在命令行模式下无法使用Java Main 20这种句子获取数据“20”,而是必须在第一次回车后才能获取参数。所以为了使用Java Main 20作为命令,应使用n=Integer.parseInt(args[0]);来获取输入的参数。]
实现了加括号的功能,我让我的题按照1/3的几率出现带有括号的题目,在所有带有括号的题目里随机出现一对括号或者两对括号,虽然题目要求中有“算式中存在的括号必须大于2个”,但又有“且不得超过运算符的个数”,所以考虑到这个,我在所有三个运算符的式子里都不出现两对即四个括号,只在四个和五个运算符的式子里出现四个括号。
实现了分数运算功能,分数题约占总题数的1/4。
比较满意的代码片段:
片段一:
在本次作业中,老师要求使用“÷”来作为输出时的除号,但计算机在计算结果时使用的是“/”来作为除号,所以为了正确的计算出结果,我将生成的运算式分成两种,一种除号使用“÷”,用做输出,另一种除号使用“/”,用作计算,分别放在两个ArryList中。
1 ArrayList<String> ex1=new ArrayList<String>();//用于显示,使用÷ 2 ArrayList<String> ex2=new ArrayList<String>();//用于计算,使用/
片段二:
为了使所出的题有更好的锻炼效果,我添加一小段代码使得相邻的两个运算符不会是同一种。这样使我出的题更多样化,更能锻炼人的计算能力。
1 int[] os=new int[2]; 2 os[j%2]=rd.nextInt(4);//随机选择一个运算符 3 int o;//本次添加的运算符 4 if(j==0){ 5 o=os[0]; 6 } 7 else{ 8 while(os[j%2]==os[(j+1)%2]){ 9 os[j%2]=rd.nextInt(4); 10 } 11 o=os[j%2]; 12 }
片段三:
对于加入括号这一部分功能,我是这样做的:
我发现左括号一定在数字的左边,右括号一定在数字的右边,这就给我的括号插入带来了便利。我将情况分为有3个运算符,有4个运算符,有5个运算符共三种情况,通过switch...case...语句分别对每种情况进行处理,得到括号对应的位置,记录到数组中。当出现括号的配对错误或者无意义时,更新导致问题出现的两个位置,直到完全正常。
我通过随机数的大小范围[rd.nextInt(10)<3]来规定出现带括号的式子的概率,增加题目的多样性。
1 if(rd.nextInt(10)<3){ 2 have=true; 3 switch(on){ 4 case 3: 5 position=new int[1][2]; 6 position[0][0]=rd.nextInt(3)+1; 7 if(position[0][0]==3){ 8 position[0][1]=100; 9 }else{ 10 if(rd.nextInt(10)<5){ 11 position[0][1]=position[0][0]+1; 12 }else{ 13 position[0][1]=position[0][0]+2; 14 } 15 }break; 16 case 4: 17 …… 18 break; 19 case 5: 20 …… 21 break; 22 } 23 }
[此部分代码过长,故仅展示有三个运算符的情形]
在生成括号的位置之后,在组合数字和运算符的过程中找准时机进行插入。
[此部分代码较长,故仅展示有一对括号的情形]
1 if(position.length==1){ 2 if(position[0][0]==(j+1)){ 3 ex_1+=String.valueOf(brackets[0])+nums[j]; 4 ex_2+=String.valueOf(brackets[0])+nums[j]; 5 flag++;//未配对的左括号的个数 6 }else if(position[0][1]==(j+1)){ 7 ex_1+=nums[j]+String.valueOf(brackets[1]); 8 ex_2+=nums[j]+String.valueOf(brackets[1]); 9 flag--;添加一个右括号后未配对左括号个数减一 10 } 11 else{ 12 ex_1+=nums[j]; 13 ex_2+=nums[j]; 14 } 15 }else{ 16 …… 17 }
【在连接符号的时候必须使用String.valueOf(),不然无法连接。】
片段四:
对于分数出题这里,我是这样做的:
建一个二维数组存放分子和分母,在随机出这些数时,每当出完一对分子和分母后,就进行化简运算,代码如下:
1 int k=nums[i][0]; 2 while(k>1){//化简 3 if((nums[i][1]%k==0)&&(nums[i][0]%k==0)){ 4 nums[i][1]/=k; 5 nums[i][0]/=k; 6 if(nums[i][0]<k){ 7 k=nums[i][0]; 8 continue; 9 } 10 } 11 k--; 12 }
由于在分数运算这里只存在加减运算,所以不必考虑优先级的问题,一个一个数字挨着算下来即可。每次我都先连接运算式,然后计算这一步的结果,存到最后一个运算过的数字的位置,然后循环直到所有的数字全部运算完毕,最后将结果连接到尾部,返回这个运算式字符串。在主函数中我将这些分数运算式统一存到一个ArrayList中,当做参数传入write方法中,由write方法输出到文件中。
软件设计的模块化原则:
我将我的程序分为六个部分,分别完成六种功能。除了主函数,我写了五个函数来完成这个程序中的五大块功能,分别是:
- 生成一个符合标准的四则运算式
- 检查数字是否可以整除,如不能则不断改变,直到获得可以整除的数字。
- 将所有的四则运算式计算出结果,并在计算过程中将不符合条件的式子进行替换。
- 生成一个使用真分数运算的式子。
- 将整合完毕的所有式子和我的学号输出到文件中。
这样完成这个需求所需要的几个大的步骤都被我分开在几个模块里了。将代码按照不同的功能分开后,想要修改哪一个部分的代码只需要在相应的函数中修改即可,就不用像没有分开时那样在一大段代码中寻找了。
在完成项目的过程中,我本来将生成运算式的代码放到了主函数中,但是在之后优化时,我发现这样写代码的话就很不灵活,如果想要在别的地方也得到一个符合要求的四则运算式就变得很繁琐,于是我就将这一部分代码也拿出来做成了单独的一个函数。
经过这样的修改我彻底将这段程序变成了几个模块,在一定程度上让我的程序有了更好的独立性,稳定性和可移植性。
PSP:
PSP2.1 |
任务内容 |
计划共完成需要的时间(min) |
实际完成需要的时间(min) |
Planning |
计划 |
10 |
8 |
· Estimate |
· 估计这个任务需要多少时间,并规划大致工作步骤 |
10 |
15 |
Development |
开发 |
423 |
665 |
· Analysis |
· 需求分析 (包括学习新技术) |
10 |
20 |
· Design Spec |
· 生成设计文档 |
10 |
10 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
5 |
5 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
3 |
5 |
· Design |
· 具体设计 |
10 |
10 |
· Coding |
· 具体编码 |
360 |
480 |
· Code Review |
· 代码复审 |
10 |
15 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
15 |
120 |
Reporting |
报告 |
15 |
23 |
· Test Report |
· 测试报告 |
6 |
10 |
· Size Measurement |
· 计算工作量 |
3 |
3 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
6 |
10 |
在整个个人项目的完成过程中,我在具体编码的这个方面花费的时间最多,这主要是因为我对于编写程序还不是很熟悉,这归咎于我平时理论看得多,实践做得少,所以突然要开始上手具体编写程序时就有一些懵了,虽然知道有这些方法可以使用,但是在具体使用的时候还是会出现这样那样的错误;更有一些只是知道可以这样做,具体应该怎样做还要随时检索,这就大大拖慢了我的速度。经过这一次的个人项目,让我认识到了我在实践方面的不足,也激励我在今后的学习中多动手,多实践。
在测试阶段我需要的时间估计和实践相差巨大。我本来以为在这个阶段就只需要用不同方法多运行几次,测试一下即可,可是我漏了我的代码会出现bug这种情形,所以为了将代码的功能调试到在所有情况下都正常,我花费了大量时间使用Debug模式寻找,修改错误。