代码仓库地址:https://git.coding.net/Siamese_miao/fourArithmetic.git
测试效果见result.txt文件
一、 需求分析与功能设计
1. 使用JAVA编程语言。
2. 接收一个输入参数n,随机产生n道四则运算练习题,符号用+-*÷来表示。
3. 每个数字在 0 和 100 之间。
4. 运算符在3个到5个之间并且每道练习题至少要包含2种运算符。
5. 运算过程中不得出现负数与非整数。
6. 将学号和生成的练习题及对应答案输出到文件“result.txt”中,不输出额外信息。
附加需求
1. 产生带括号的四则运算并求解,算式中存在的括号必须大于2个,且不得超过运算符的个数。
2. 产生真分数的加减法运算,运算过程中都为最简真分数。
基本功能
能够根据用户输入的参数n随机产生n道符合要求的四则混合运算练习题,自动算出答案,并将式子与答案以文档的形式呈现。
扩展功能
支持有括号的运算、支持最简真分数的加减运算
二、 设计实现
首先,我从简单四则混合运算开始,接着在这的基础上完成了分数运算,最后再思考带括号的运算。我先按这个顺序分别创建了项目,单独编写,最后才分类规整到一个项目中,如图我分为5个类。
- Main类:主类,负责接收命令行的参数并启动程序。
- fileCreate类:创建文件类,负责每次出题类型并产生result.text文件,将学号与练习题写入文件。
- formula类:式子类,负责根据调用产生同种类型的式子,含有arithmetic(简单四则运算)、bracket(带括号的四则运算)、score(真分数加减运算)三种函数。
- calculate类:计算类,负责各种计算,含有结果运算、有条件产生减数、有条件产生除数、有条件产生分子、有条件产生分母、判断数的大小、求取最大公因数、求取最小公倍数、分数相加、分数相减等10个方法。
各类与各函数之间的关系如下图。
三、 算法详解
我的算法大多数比较简单,多采用递归的方式。
-
简单四则运算
由于符号由+-*÷表示,而计算机计算使用+-*/,所以我先定义了两个符号数组与定义两个字符串,一个作为显示式子用,一个用于计算,数组下标一一对应。再由范围为3~5的随机数确定式子的符号数,再由两个整型数组存储算数与符号对应的下标值,值皆由随机数产生,算数范围为0~100,下标值范围为0~3。式子使用for循环,每次将一个算数与一个符号加入字符串中,执行完循环后,最后再加上最后一个算数。计算后,连同式子一并输出。在计算时,我使用java调用js中的eval(String)函数求解。因为式子必须包含两种运算符以上,在输出前需要判断是否所有符号相同,不相同则不输出并重新运行程序。
1 // 计算结果 2 public static Object result(String temp) { 3 ScriptEngineManager sem = new ScriptEngineManager(); 4 ScriptEngine se = sem.getEngineByName("js"); 5 Object last = 0; 6 try { 7 last = se.eval(temp); 8 } catch (ScriptException e) { 9 e.printStackTrace(); 10 } 11 return last; 12 }
考虑到运算过程中不能出现小数与负数,所以在除号与减号部分需要做处理,当减数大于被减数时,重新生成减数直至不大于被减数,当除数无法整除被除数或为0时,重新生成除数。后来在测试时发现两两数字符合规则,然而整条式子却仍会出现负数与小数。经过思考,一般出现于连减、连除、减号后乘除因优先级不同的式子中,所以我设了个判断,减号后一位只能为加号,除号后一位只能为加减号,由此解决了运算过程中出现负数或小数的问题。
1 public static String arithmetic() { 2 int m, j; 3 char[] p = new char[] { '*', '+', '÷', '-' }; 4 char[] q = new char[] { '*', '+', '/', '-' }; 5 String temp1 = ""; 6 String temp2 = ""; 7 m = (int) (Math.random() * 3 + 3); // 符号数 8 int[] num = new int[m + 1]; // 数字 9 int[] key = new int[m]; // 符号所在的下标 10 for (j = 0; j <= m; j++) { 11 num[j] = (int) (Math.random() * 101); 12 } 13 for (j = 0; j < m; j++) { 14 if (j > 0 && key[j - 1] == 3) { // 减号后仅允许加号,防止负数出现 15 key[j] = 1; 16 } else if (j > 0 && key[j - 1] == 2) { 17 key[j] = (int) (Math.random() * 2); // 除号后仅允许乘号与加号,防止负数 18 } else { 19 key[j] = (int) (Math.random() * 4); // 随机符号 20 } 21 temp1 += String.valueOf(num[j]) + String.valueOf(p[key[j]]); 22 temp2 += String.valueOf(num[j]) + String.valueOf(q[key[j]]); 23 if (key[j] == 3) { 24 num[j + 1] = calculate.decide1(num[j], num[j + 1]); // 选定小于被减数的减数 25 } else if (key[j] == 2) { 26 num[j + 1] = calculate.decide2(num[j], num[j + 1]); // 确保能够整除 27 } 28 } 29 j = 0; 30 while (j < (m - 1) && key[j] == key[j + 1]) 31 j++; // 与第一个符号相同数 32 if (j == (m - 1)) 33 return arithmetic(); // 若所有符号相同,该式子不算,保证有两种运算符 34 else { 35 temp1 += String.valueOf(num[m]); 36 temp2 += String.valueOf(num[m]); 37 return temp1 + "=" + calculate.result(temp2); 38 } 39 }
-
分数的加减
在基本混合运算的基础上修改出分数加减就显得简单许多。最主要的就是分数的通分约分,解决了这个问题,程序就基本完成了。最大公因数我之前使用了for循环挨个计算,今天(3月28日)我想起了辗转相除法,修改之后,运算速度明显提高。
1 public static int gcd(int x, int x2) { 2 int s = 1; 3 x = Math.abs(x); 4 x2 = Math.abs(x2); 5 while (x2 != 0) { 6 s = x % x2; 7 x = x2; 8 x2 = s; 9 } 10 return x; 11 }
-
含括号的运算
这部分由于优先级的改变以及时间关系,经过改善,目前已经基本确保运算结果不含小数与负数。
1 // 判断是否为小数 2 public static boolean judgeIsDecimal(String num) { 3 boolean isdecimal = false; 4 if (num.contains(".")) { 5 isdecimal = true; 6 } 7 return isdecimal; 8 }
我思考了很久如何随机生成括号,最后受到一篇博客(当时忘记保存网址,现在已经找不到了,抱歉)使用概率的启发,我以一定概率的方式生成左括号,记录生成左括号的个数以及未匹配的左括号的个数,以一定概率生成右括号,最后补齐。
1 if (((brack * 2) <= (n - 1)) && (((int) (Math.random() * 2)) == 0)) // 以一定概率生成左括号,概率为1/2 2 { 3 temp1 += "("; 4 temp2 += "("; 5 brack++; 6 brack_left++; 7 temp1 += num[++i]; // 生成左括号后必须生成一个数字和运算符,不然可能出现(15)这样的错误 8 temp2 += num[i]; 9 op = (int) (Math.random() * 4); 10 temp1 += Op[op]; 11 temp2 += p[op]; 12 if (op == 3) 13 div = 1; 14 else if (op == 1) 15 div = 2; 16 }
四、 测试运行
进入src文件下,输入javac -encoding utf-8 Main.java 编译出相应的class文件,再输入java Main 20进行测试,我们可以先测试java Main abc或java Main 1500或java Main 0,在这里我使用的jdk版本为jdk1.8.0_25。
测试结果如下图。
除此之外,我还学习了使用myeclipse做单元测试,测试结果如图所示,我从测试结果发现,当文件存在时,删除重建会耗时约2秒(以5道算式为例)。
五、代码片段
分数的加减计算因为没有优先级的关系,可以一边补充式子一边计算从而修改算数,无需限定符号。这也是我最满意的代码。
1 // 真分数分式 2 public static String score() { 3 char[] p = new char[] { '+', '-' }; 4 int j; 5 String temp1 = ""; 6 int m = (int) (Math.random() * 3 + 3); 7 int[] key = new int[m]; // 运算符 8 int[] x = new int[m + 1]; // 分子 9 int[] y = new int[m + 1]; // 分母 10 int[] sum = new int[2];// 中途运算结果 11 for (j = 0; j <= m; j++) { 12 x[j] = (int) (Math.random() * 20 + 1); 13 y[j] = calculate.decide3(x[j]); 14 } 15 sum[0] = x[0]; 16 sum[1] = y[0]; 17 for (j = 0; j < m; j++) { 18 key[j] = (int) (Math.random() * 2); 19 if (key[j] == 0) { // 结果小于1 20 int[] num = new int[2]; 21 num = calculate.fracAdd(sum[0], sum[1], x[j + 1], y[j + 1]); 22 if (num[0] >= num[1]) { 23 key[j] = 1; 24 } else { 25 sum = num; 26 } 27 } 28 if (key[j] == 1) { // 结果不为负数 29 int[] num = new int[2]; 30 num = calculate.fracSub(sum[0], sum[1], x[j + 1], y[j + 1]); 31 if (num[0] < 0) { 32 x[j + 1] = calculate.decide4(sum[0], sum[1]); 33 y[j + 1] = sum[1]; 34 num = calculate.fracSub(sum[0], sum[1], x[j + 1], y[j + 1]); 35 } 36 sum = num; 37 } 38 temp1 += String.valueOf(x[j]) + "/" + String.valueOf(y[j]) + String.valueOf(p[key[j]]); 39 } 40 j = 0; 41 while (j < (m - 1) && key[j] == key[j + 1]) 42 j++; // 与第一个符号相同数 43 if (j == (m - 1)) 44 return score(); // 若所有符号相同,该式子不算,保证有两种运算符 45 else { 46 temp1 += String.valueOf(x[m]) + "/" + String.valueOf(y[m]); 47 return temp1 + "=" + sum[0] + "/" + sum[1]; 48 } 49 }
六、 总结
一开始由于我不清楚我能否完成附加功能,所以我用一个Main类的主函数完整的写一个简单的四则运算。但是调试时花费了许多时间,并且十分不灵活,所以我把函数中重复的计算部分抽离出来,写成静态方法,再进行测试,调试起来方便许多,想要修改哪一个部分的代码只需要在相应的函数中修改即可。同理,分数运算与括号运算我也这么做,由此我创建了三个项目。当三个项目都差不多完成时,我意识到其中有许多重复之处,所以我将它们分类并合在一个项目中。
除了主类包含了一个主函数外,我把我的程序分成3个类,各自分工,每个类中我尽量把各个功能细分成各种小方法,尤其在calculate类中每个方法不超过10行。具体可看第三点的关系图。方法间的逐级调用给调试和测试都带来了很多便利,尤其在测试优化方面,同时也增加了代码的可移植性和可读性。
七、 PSP
PSP2.1 |
任务内容 |
计划共完成需要的时间(min) |
实际完成需要的时间(min) |
Planning |
计划 |
30 |
20 |
· Estimate |
· 估计这个任务需要多少时间,并规划大致工作步骤 |
30 |
20 |
Development |
开发 |
1515 |
2470 |
· Analysis |
· 需求分析 (包括学习新技术) |
180 |
150 |
· Design Spec |
· 生成设计文档 |
50 |
30 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
10 |
15 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
5 |
5 |
· Design |
· 具体设计 |
10 |
15 |
· Coding |
· 具体编码 |
1200 |
1800 |
· Code Review |
· 代码复审 |
30 |
15 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
30 |
440 |
Reporting |
报告 |
75 |
170 |
· Test Report |
· 测试报告 |
10 |
15 |
· Size Measurement |
· 计算工作量 |
5 |
5 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
60 |
150 |
这次的代码我思考了很久,在敲代码与测试方面远远的超过了计划的时间,源于我低估了代码的复杂度。虽然有思路,但把思路实现却花费了特别多的时间,思虑的不充分总是让程序报错,再加上我对java掌握的不够,所以我在编程花费了许多许多时间,有很多时候就是不知如何实现需求。再者,打这篇博客报告也花费了很多时间,但也再次整体的梳理了我的思路。
在这次打代码的过程中,我学到了四个方法。
一个是使用java调用js的eval()函数,这个方法可以输入字符串型的算式然后直接算出答案。
另外一个是在用命令运行符使用Java Main 20作为命令时,应使用
1 n=Integer.parseInt(args[0]);
来获取输入的参数,而我在myeclipse使用的一直是
1 Scanner in = new Scanner(System.in); 2 n=in.nextInt();
语句执行。
还有一个是以概率的方式随机生成括号。
最后是使用Debug修改错误代码,使用单元测试优化程序提高性能。
这次的学习让我意识到自己的不足,我以后会继续努力。