2016012078 小学四则运算练习软件项目报告
Coding.net原码仓库地址:https://git.coding.net/Meloody/Code.git
一、需求分析(前提)
话说需求分析是一份体力活儿,也是一份技术活儿,它既是人际交往的艺术,又是逻辑分析与严密思考的产物。
具有理解能力、设计能力是分析需求必备的技能。一个UML图可以清晰地展示出需求分析。(参考博客1)但考虑此次作业涉及领域相对较小,客户需要一个四则运算的命令行软件,所以不采用UML图。
-
输入 n 人性化地决定生成题目的个数
-
运算符为:+-*÷
-
每个数字在0-100之间,运算符3-5个
-
每个练习题至少要包含2种运算符
-
运算过程中不出现负数与非整数
-
设置参数控制生成题目数量、运算符个数、计算数产生范围
- 使用jdk8u161版本
- 使用jre8u161版本
-
学号与题及其正确答案输出到文件“result.txt”
二、功能设计(价值)
基本功能:
-
实现四则运算出题,随机生成运算式。
扩展功能:
-
计算分数的加减法 例如:16/5-7/3/7*3/5 = 3
-
运算式去重
三、设计实现(保障)
设计包括你会有哪些类,这些类分别负责什么功能,他们之间的关系怎样?你会设计哪些重要的函数,关键的函数是否需要画出流程图?函数之间的逻辑关系如何?
设计包括的类及负责的功能,他们之间的关系:
(一) 整体上,project项目/src下建立Main.java和WriteToFile.java文件。
涉及重要函数和函数流程图及逻辑关系:
(一) 导入类和函数:
int random(int x) 随机数类,这个类的实例用于生成伪随机数流
import java.util.Random;
ArrayList(),有序可重复的集合,其底层就是数组
import java.util.ArrayList;
Scanner,属于SDK1.5新增的一个类,可是使用该类创建一个对象
import java.util.Scanner;
BufferedWriter,缓冲字符输出流,继承于Writer,为其他字符输出流添加一些缓冲功能
import java.io.BufferedWriter;
FileWriter,字符流,用法和FileInputStream和FileOutputStream一样,只是应用的范围不一样(文件字符流用于纯文本文件的读写比较快速)
import java.io.FileWriter;
printStackTrace(),是打印出异常,还显示调用信息
e1.printStackTrace();
eval()函数,计算字符串内的运算式
ax=ax+"="+jse.eval(ax);
(二) 流程图如下:
四、算法详解(灵魂)
(一) 当被除数不能整除除数时,随机生成能够整除的除数
private static int dec(int x,int y){ Random random=new Random(); if(x%y!=0)//
{ y=random.nextInt(100)+1; return dec(x,y); } else{ return y; } }
(二) 通过javascript的eval函数计算字符串内的运算式
/** * 计算等式结果,返回运算式 */ static ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript"); private static ArrayList<String> calculate(ArrayList<String> arrayList){ ArrayList<String> ArithExpress=new ArrayList<String>(); for(String ax:arrayList){ try { ax=ax+"="+jse.eval(ax); System.out.println(ax); ArithExpress.add(ax); } catch (ScriptException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return ArithExpress; }
(三)逆波兰式(相似调度场)的求值:
逆波兰式式也称后缀表达式。一般求值表达式都是中缀表达式,而后缀表达式求值更容易,所以将中缀表达式转换为后缀表达式。
步骤:
第一步:将中缀表达式转换为后缀表达式。
1,将+,-,*,/,(等要用到的运算进行优先级处理)
2,需要用到一个符号栈处理:
a,数字字符,小数点直接存入另一个字符串S。
b, "( "直接入栈,当表达式str字符中")"出现,将所有栈顶运算符转移至S直至遇到"(","( "出栈。
c,当表达式str字符中的运算符优先级大于等于(注意一定要大于等于)栈顶运算符优先级时,将字符依次存入S,直至小于为止。当前运算符入栈。
d,最后当str访问完,将栈中剩余运算符存到S。
第二步:将后缀表达式求值。
1,需要一个浮点型栈(具体根据题目要求)存放数值。
2,遍历S遇见操作数,小数点时处理后入栈,直至遇见运算符,出栈两个操作数,处理后将结果再入栈。
3,栈顶元素即为表达式解。
eg. 根据后缀表达式:12 34 11 99 * / - 计算结果。
从左到右遍历后缀表达式,
遇到数字就进栈,
遇到符号,就将栈顶的两个数字出栈运算,运算结果进栈,直到获得最终结果。
1 import java.util.ArrayList; 2 import java.util.Iterator; 3 import java.util.List; 4 import java.util.Stack; 5 6 /* 7 *测试 -5*(10/(2*4.5-4)+(-3/1.5+4)*(-2))/(-2/1-(-1))+12=2.0 8 */ 9 public class Calc { 10 // 求四则运算表达式运算结果 11 public static double excute(String value) throws Exception { 12 List<String> list = toList(value);// 按顺序转成数字符号list 即中序表达式 13 list = toSuffixExpressionList(list);// 转成逆波兰式数字符号list 即后序表达式 14 double result = suffix_excute(list);// 求逆波兰式结果 15 System.out.println(value + "=" + result); 16 return result; 17 } 18 19 // 表达式划分成中序list 即从左到右数字符号分开 20 private static List<String> toList(String value) { 21 // 开始为-时加上0 22 if ("-".equals(value.substring(0, 1))) { 23 value = "0" + value; 24 } 25 int begin = 0; 26 int end = 0; 27 String item; 28 List<String> resultList = new ArrayList<String>(); 29 for (int i = 0, len = value.length(); i < len; i++) { 30 item = value.substring(i, i + 1); 31 if (isOperator(item)) { 32 // 负数跳过 33 if ("-".equals(item) && "(".equals(value.substring(i - 1, i))) { 34 continue; 35 } 36 end = i; 37 // 前一个非符号时加上数字 38 if (begin != end) { 39 resultList.add(value.substring(begin, end)); 40 } 41 // 加上符号 42 resultList.add(value.substring(end, end + 1)); 43 begin = end + 1; 44 } 45 } 46 // 加上最后一个数字 47 if (begin != value.length()) { 48 resultList.add(value.substring(begin)); 49 } 50 // System.out.println(value + "=" + list); 51 return resultList; 52 } 53 54 // 中序list转换成逆波兰式list 左右根 55 private static List<String> toSuffixExpressionList(List<String> list) throws Exception { 56 Stack<String> operatorStack = new Stack<String>();// 符号栈 57 Stack<String> resultStack = new Stack<String>();// 结果栈 58 Iterator<String> iter = list.iterator(); 59 while (iter.hasNext()) { 60 String item = iter.next(); 61 if (isOperator(item)) { 62 if (")".equals(item)) { 63 // 遇到)时符号栈一直弹出并压入结果栈直到遇到(,弹出(废弃,结束。 64 while (!(operatorStack.isEmpty() || "(".equals(operatorStack.peek()))) { 65 resultStack.push(operatorStack.pop()); 66 } 67 // 弹出( 68 if (!operatorStack.isEmpty() && "(".equals(operatorStack.peek())) { 69 operatorStack.pop(); 70 } else { 71 throw new Exception("(少了"); 72 } 73 } else if ("(".equals(item)) { 74 // 遇到(时直接入符号栈,结束 75 operatorStack.push(item); 76 } else { 77 // 遇到其他运算符时与符号栈顶(若符号栈顶为空或(时直接入符号栈,结束)运算比较 若比栈顶高直接入符号栈,结束 78 // 否则符号栈弹出并压入结果栈 并再执行与符号栈顶比较直到弹入符号栈,结束 79 while (!(operatorStack.isEmpty() || "(".equals(operatorStack.peek()))) { 80 if (compareOperator(item, operatorStack.peek()) < 1) { 81 resultStack.push(operatorStack.pop()); 82 } else { 83 break; 84 } 85 } 86 operatorStack.push(item); 87 } 88 89 } else { 90 // 数字时直接入结果栈 91 resultStack.push(item); 92 } 93 } 94 // 符号栈全部弹出并压入结果栈 95 while (!operatorStack.isEmpty()) { 96 if ("(".equals(operatorStack.peek())) { 97 throw new Exception("(多了"); 98 } 99 resultStack.push(operatorStack.pop()); 100 } 101 // 结果栈弹出并反序得出最终结果 102 iter = resultStack.iterator(); 103 List<String> resultList = new ArrayList<String>(); 104 while (iter.hasNext()) { 105 resultList.add(iter.next()); 106 } 107 // System.out.println(list + "=" + rtList); 108 return resultList; 109 } 110 111 // 逆波兰式list 求值 112 // 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 op 113 // 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。 114 private static double suffix_excute(List<String> list) { 115 Stack<Double> resultStack = new Stack<Double>(); 116 Iterator<String> iter = list.iterator(); 117 Double num1; 118 Double num2; 119 while (iter.hasNext()) { 120 String item = iter.next(); 121 if (isOperator(item)) { 122 num2 = resultStack.pop(); 123 num1 = resultStack.pop(); 124 resultStack.push(doOperator(num1, num2, item)); 125 } else { 126 resultStack.push(Double.parseDouble(item)); 127 } 128 } 129 return resultStack.pop(); 130 } 131 132 // 比较两运算高低 1 1>2, 0 1=2 -1 1<2 133 private static int compareOperator(String operator1, String operator2) { 134 if ("*".equals(operator1) || "/".equals(operator1)) { 135 return ("-".equals(operator2) || "+".equals(operator2)) ? 1 : 0; 136 } else if ("-".equals(operator1) || "+".equals(operator1)) { 137 return ("*".equals(operator2) || "/".equals(operator2)) ? -1 : 0; 138 } 139 // 这个到不了 140 return 1; 141 } 142 143 // +-*/基本运算 144 private static double doOperator(Double num1, Double num2, String operator) { 145 if ("+".equals(operator)) { 146 return num1 + num2; 147 } else if ("-".equals(operator)) { 148 return num1 - num2; 149 } else if ("*".equals(operator)) { 150 return num1 * num2; 151 } else if ("/".equals(operator)) { 152 return num1 / num2; 153 } 154 // 这个到不了 155 return -1; 156 } 157 158 // 是否为运算符 159 private static Boolean isOperator(String value) { 160 return "(".equals(value) || ")".equals(value) || "+".equals(value) || "-".equals(value) || "*".equals(value) 161 || "/".equals(value); 162 } 163 164 public static void main(String[] args) { 165 try { 166 excute("-5*(10/(2*4.5-4)+(-3/1.5+4)*(-2))/(-2/1-(-1))+12"); 167 } catch (Exception e) { 168 e.printStackTrace(); 169 } 170 } 171 }
参考一看就懂的大神博客2
(四)设置参数控制生成运算符个数、计算数产生范围:
for(int i=0;i<num;i++){ int n=random.nextInt(3)+3; //3-5个运算符 int[] number=new int[n+1]; String ex=new String(); for(int j=0;j<=n;j++){ number[j]=random.nextInt(100)+1; //4-5个数字 }
(五)调用ScriptEngine脚本:
static ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript"); try { BufferedWriter bw = new BufferedWriter(new FileWriter(path)); bw.write("2016012078"); bw.newLine(); for(String con:content){ bw.write(con); bw.newLine(); } bw.close(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace();//printStackTrace()方法是打印出异常,还显示调用信息. }
五、测试运行(成果)
程序的运行截图展示实现功能满足项目需求:
(一)运行环境jdk-8u161-windows-x64.exe;
输入java、javac、java -version,java -version回车会返回三行信息如下(第一行表明Java语言的语法版本):
(二)命令行测试(清晰步骤见图):
注意:我在测试的时候发现命令行测试会出现gbk编码错误,所以要输入:javac -encoding utf-8 Main.java
(三) result.txt文件结果:
(四) Console运行结果:
六、满意代码(精彩)
(一) 随机产生num个运算式,将产生的运算式存入List集合:
char[] operator=new char[]{'+','-','*','/'}; Random random=new Random(); ArrayList<String> expression=new ArrayList<String>(); for(int i=0;i<num;i++){ int n=random.nextInt(3)+3; //3-5个运算符 int[] number=new int[n+1]; String ex=new String(); for(int j=0;j<=n;j++){ number[j]=random.nextInt(100)+1; //4-5个数字 } for(int j=0;j<n;j++){ int s=random.nextInt(4);//随机选择某运算符 ex+=String.valueOf(number[j])+String.valueOf(operator[s]); if(s==3){number[j+1]=decide(number[j],number[j+1]);} } ex+=String.valueOf(number[n]); expression.add(ex); }
(二)整型转化为字符型时(Bug调试5小时),借助double类型:
double result=0; try { result = Double.parseDouble(String.valueOf(jse.eval(ex))); } catch (ScriptException e) { e.printStackTrace(); } if(Math.floor(result)==result&&result>0&&result<99999) { System.out.println(ex + "=" + (int) result); expression.add(ex); } else i--; }
(
public static String sim(int a,int b){ int y = 1; //求分子分母的最小公因数 for(int i=a;i>=1;i--){ if(a%i==0&&b%i==0){ y = i; break; } } int z = a/y; int x = b/y; if(z==0)//分子为0则结果为0
{ return "0"; } return ""+z+"/"+x; }
public static ProperFraction cal(ProperFraction a, ProperFraction b, Symbol s){ ProperFraction p = null; //计算分数加减乘除 switch (s.getValue()){ case "+":p = fracAdd(a.numa,a.numb,b.numa,b.numb);break; case "-":p = fracSub(a.numa,a.numb,b.numa,b.numb);break; case "*":p = fracMul(a.numa,a.numb,b.numa,b.numb);break; case "/":p = fractDiv(a.numa,a.numb,b.numa,b.numb);break; } return p; }
代码去重,
七、总结反思(提升)
模块化这个词最早出现在研究工程设计的《Design Rules》中,模块化是以分治法为依据。
简单说就是把软件整体划分,划分后的块组成了软件。我设计的程序通过需求分析构架软件、分析功能、运用类及函数,大而化小,逐步实现软件设计。Bug调试,我依旧坚持挺过来了,还记得最清楚的一个Bug,整型转化为字符型的一个小细节,花了5小时左右。
这次项目,是一次很大的锻炼,我尝试解决平时没有遇到的问题,遇到问题会焦急。但是,看着自己解决了一个个模块,我也异常兴奋。这次项目,从最初的分析设计,中期编码,到后期调试,整个过程可以说,每一步都充满了挑战,但我相信艰辛过后就是甜蜜的幸福。我做到了,见到彩虹,依然微笑着。
经验上,下次做项目,我会更加从大处着手,小细节交给后期,高效率也是一门艺术。
八、PSP展示(记录)
|
任务内容 |
计划共完成需要的时间(min) |
实际完成需要的时间(min) |
Planning |
计划 |
400 |
811 |
· Estimate |
· 估计这个任务需要多少时间,并规划大致工作步骤 |
2天 |
4天 |
Development |
开发 |
375 |
800 |
· Analysis |
· 需求分析 (包括学习新技术) |
70 |
150 |
· Design Spec |
· 生成设计文档 |
20 |
15 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
3 |
5 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
8 |
10 |
· Design |
· 具体设计 |
10 |
20 |
· Coding |
· 具体编码 |
200 |
200 |
· Code Review |
· 代码复审 |
4 |
30 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
60 |
360 |
Reporting |
报告 |
10 |
0 |
· Test Report |
· 测试报告 |
3 |
0 |
· Size Measurement |
· 计算工作量 |
2 |
1 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
10 |
10 |
问题:这个环节重要的是让自己看到自己的估计和实际消耗时间,哪个环节耗时最多,哪个环节估计和实践相差巨大?为什么?
估计和实际的总时间相差了近一倍,主要原因是代码能力不足,好好反思后,继续多多编程,认真写项目。在测试(自我测试,修改代码,提交修改)阶段耗时最多,同时也是和估计差距最大的一个环节,主要是因为自身对Bug调试的主观回避,但是这次之后,我会客观估计时间,做出更加合理的计划,对项目开发有一个更深刻的认识。
我还有分享的就是我做项目的心情状态,我承认这个过程不是三言两语就可以让承受地压力风轻云淡,但是我始终有信念,任何一件事,花出时间和实践就会有收获!
1. 我们应该怎么做需求分析 https://www.cnblogs.com/mengfanrong/p/4594710.html
2. java四则运算-通过逆波兰式求值http://tianyami.iteye.com/blog/1462498