2017202110105-高级软件工程2017第2次作业
GitHub地址
https://github.com/setezzy/MyCalculator
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
·Estimate | · 预估时间 | 10 | 10 |
Development | 开发 | 570 | 930 |
·Analysis | · 需求分析(包括学习新技术) | 30 | 40 |
·Design Spec | · 生成设计文档 | 20 | 30 |
·Design Review | · 设计复审 | 20 | 30 |
·Coding Standard | · 代码规范 | 30 | 30 |
·Design | · 具体设计 | 60 | 100 |
·Coding | · 具体编码 | 240 | 480 |
·Code Review | · 代码复审 | 30 | 40 |
·Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | 150 | 150 |
·Test Report | · 测试报告 | 120 | 120 |
·Size Measurement | · 计算工作量 | 20 | 20 |
·Postmortem&Process Improvement Plan | · 事后总结并提出过程改进计划 | 30 | 30 |
合计 | 730 | 1090 |
需求分析
目标
本软件的主要目标为:四则混合运算题目生成及计算
用户特点
本软件最终用户为小学生(或小学教师),会基本的四则混合运算,不会负数计算。故我们应基于上述用户特点进行软件开发。
假定和约束
开发方法:面向对象的开发技术
开发语言:Java
开发期限:1周
需求规定
由于软件规模较小,在此只考虑功能需求。以小学生为主要服务对象,对软件的功能初步设定为:
a.表达式生成功能。分为操作数随机生成,运算符随机生成以及最终表达式生成。操作数包括100以内正整数和真分数;运算符包括( +, -, *, ÷);最终表达式需要包含带括号的子表达式。其中操作数及运算符序列个数也随机生成。
b. 表达式计算功能。要求计算带括号的混合运算的结果,若最后结果为分数,则需要对分数进行化简。
c. 用户交互功能。首先判断用户输入是否非法,例如输入字母时要求用户重新输入;再对用户答案进行正确性判断:若正确,记得分,若错误,显示正确答案。最后显示用户的最终得分。
d. 用命令行参数n控制题目数量。
功能图
运行环境规定
设备:PC
支持:PC支持Windows, Linux等操作系统
控制:本软件主要通过cmd运行,输入命令即可控制软件,无特殊要求。
详细设计
设计思路
a. 生成操作数数组:随机生成正整数或真分数,可由随机数决定。特别注意的是生成分数时,分子应小于分母。将操作数数目作为该方法变量,循环生成操作数,存入数组。
b. 生成运算符数组:随机生成四种运算符,通过switch()实现。当生成的运算符为乘或除时,考虑插入括号,且保证括号插入在低优先级运算符两侧:若乘/除号前一个是加/减号,可以插入括号,是否插入可由随机数决定。将操作数数目作为该方法变量,循环生成操作数,存入数组,当运算符数量达到最大值时,最后插入”=”。
c. 生成表达式:将a,b步骤生成的序列按序插入就得到一个符合逻辑的表达式。首先遍历操作数数组,当有括号时,判断是否为左括号,若是,则先插入括号,再依次插入数字和运算符和对应的右括号。
d. 表达式计算:在这里并未采用逆波兰表达式计算方法,而是参考了其他实现方法:先定义无括号的方法,具体实现为将表达式拆分分别放入list(分为操作数list和运算符list),接下来取运算符,取对应索引处左右的操作数,计算,并将计算结果放回当前索引位置。有括号的方法,具体实现为查找表达式中的括号,取出括号内的子表达式,调用无括号方法进行计算,用该计算结果替换该子串,得到新的表达式,再递归调用有括号方法。
e. 用户交互:用main()函数的String args[]参数接收用户定义的题目数量,循环调用c步骤的方法生成题目;接收用户输入的答案,若不合法则要求重新输入,将用户答案与d步骤得到的运算结果匹配,若相等,则显示“正确”,并计入得分,否则显示“错误”并显示正确答案。
设计实现
程序整体流程图
类图
核心代码说明
CreateFig类
考虑到需要生成真分数,我定义了一个对象数组,方便将分子(ele)、分母(den)分开存储。整数可表示为ele/1的形式,分数表示为ele/den形式。
public class CreateFig {
public define[] fig (int count) { //定义生成操作数的数量
int flag; //是否生成分数
define[] num = new define[10];
define def;
for (int i = 0; i <= count; i++) {
def = new define();
flag = (int) (Math.random() * 100) % 3;
if (flag == 0) {
def.setDen((int) (Math.random() * 18) + 2); //分母为[2,20)内随机数
def.setEle((int) (Math.random() * def.getDen()) + 1); //分子要小于分母
num[i] = def;
} else {
def.setDen(1);
def.setEle((int) (Math.random() * 19) + 1); //生成[1,20)范围内的整数
}
}
return num;
}
}
Calculate类
方法主要参考 HeadingAlong的博客 。将表达式的运算符和操作数分别存入两个list中,遍历运算符list,先取乘除运算符,再从操作数list中取该索引两边的操作数(对应运算符左右的操作数),计算后将运算结果放回原索引处。同样地,再遍历list完成加减运算,直至list为空。
public class Calculate {
public static String calculate(String exp){
List<Character> oper = Operator(exp);
List<String> fig = Figure(exp);
for (int i = 0; i < oper.size(); i++) { //遍历运算符容器完成乘除运算
Character op = oper.get(i); //取得运算符
if (op == '*' || op == '÷') {
oper.remove(op);
String l = fig.remove(i);
String r = fig.remove(i); //取得运算符左右两侧的操作数
int lele, lden, rele, rden; //分别定义两个操作数的分子分母
List<Integer> fra = ToFrac.tofrac(l, r);
lele = fra.remove(0);
lden = fra.remove(0);
rele = fra.remove(0);
rden = fra.remove(0);
if (op == '*') { //乘法运算
fig.add(i, (lele * rele) + "/" //将运算后的数添加在i位置
+ (lden * rden));
} else {
fig.add(i, (lele * rden) + "/" //除法运算
+ (lden * rele));
}
i--;}}
while (!oper.isEmpty()) { //完成加减运算,为空时停止
String result = null;
Character op = oper.remove(0);
String l = fig.remove(0);
String r = fig.remove(0);
int lele, lden, rele, rden;
List<Integer> fra = ToFrac.tofrac(l, r);
lele = fra.remove(0);
lden = fra.remove(0);
rele = fra.remove(0);
rden = fra.remove(0);
if (op == '+') { //加法运算
result = ((lele*rden) + (lden * rele))
+ "/" + (lden * rden);
fig.add(0, result);
}
if (op == '-') { //减法运算
result = ((lele * rden) - (lden* rele))
+ "/" + (lden*rden);
fig.add(0, result);
}}
return fig.get(0);
}
/*------------提取运算符--------------*/
public static List<Character> Operator(String op){
List<Character> list = new ArrayList<>();
for (int i = 0; i < op.length(); i++) {
if (op.charAt(i) == '+' || op.charAt(i) == '-'
|| op.charAt(i) == '*' || op.charAt(i) == '÷') {
list.add(op.charAt(i));}
}
return list;
}
/*-------------提取操作数-------------*/
public static List<String> Figure(String fig){
int n = 0;
int count=0;
int k=0;
List<String> list = new ArrayList<>();
for(int i=0;i<fig.length();i++){
if (fig.charAt(i) == '+' || fig.charAt(i) == '-'
|| fig.charAt(i) == '*' || fig.charAt(i) == '÷'
|| fig.charAt(i) == '=') {
k=i; //运算符的index
count++; }
}
if(count==1){
list.add(fig.substring(0,k));
list.add(fig.substring(k+1,fig.length()));}
else {
for (int i = 0; i < fig.length(); i++) {
if (fig.charAt(i) == '+' || fig.charAt(i) == '-'
|| fig.charAt(i) == '*' || fig.charAt(i) == '÷'
|| fig.charAt(i) == '=') {
list.add(fig.substring(n, i));
n = i + 1;}}}
return list;
}}
NewCalculate类
查找表达式的括号,若无括号,则直接调用calculate();否则获取括号内的子串,调用calculate(),用计算结果替换原子串,再递归调用newcalculate()。
public class NewCalculate {
public static String newcalculate(String exp) {
int lpar = exp.indexOf('('); //在表达式中查找左括号
if (lpar == -1) {
return Calculate.calculate(exp); //若没有左括号则直接计算
} else {
int rpar = exp.indexOf(')'); //若有,则获取该左括号对应的第一个右括号
String expression = exp.substring(lpar + 1, rpar);
exp = exp + "=";
String ans = Calculate.calculate(expression); //计算括号内的表达式
if (ans.indexOf("-") != -1) {
ans = "#"
+ ans.substring(1, ans.length());
}
exp = exp.substring(0, lpar) + ans
+ exp.substring(rpar + 1, exp.length()); //用计算结果替换带括号的子串
return newcalculate(exp); //返回运算结果
}
}
}
分数化简
public class Simplify { //分数化简
public static String gcd(String exp){
int p=exp.indexOf('/');
int ele,den,r,m,n=0;
String result=null;
ele=Integer.parseInt(exp.substring(0,p));
den=Integer.parseInt(exp.substring(p+1,exp.length()));
if(ele>den) {
m=ele;
n=den;}
else{
m=den;
n=ele;}
r=m%n;
while (r != 0) { //辗转相除法求最大公约数
m = n;
n = r;
r=m%n;
}
if(den/n==1)
result=String.valueOf(ele/n);
else
result = (ele / n) + "/" + (den / n);
return result;}
}
测试运行
程序运行结果如下图所示:
当用户输入不合法时,需重新输入:
测试方法:单元测试
测试对象:CreateExp() NewCalculate() ToFrac() Operator() Figure()
测试结果:测试均通过。
代码覆盖率如下(由于部分基础方法没有测试,故method coverage不为100%):
测试代码:
CreateExp
public class CreateExpTest extends TestCase {
public void testexp() throws Exception{
CreateExp ce=new CreateExp();
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine engine = mgr.getEngineByName("JavaScript");
for(int i=0;i<1000;i++) {
String str = ce.exp((int) ((Math.random() * 100) % 4 + 3)).replace('÷', '/');
Assert.assertNotNull(engine.eval(str).toString());
}
}
}
ToFrac
public void testToFrac() throws Exception{
List<Integer> list1 = new ArrayList(Arrays.asList(2,1,3,1));
List<Integer> list2 = new ArrayList(Arrays.asList(2,3,4,1));
List<Integer> list3 = new ArrayList(Arrays.asList(5,1,5,6));
List<Integer> list4 = new ArrayList(Arrays.asList(1,2,2,3));
Assert.assertEquals(list1,ToFrac.tofrac("2","3"));
Assert.assertEquals(list2,ToFrac.tofrac("2/3","4"));
Assert.assertEquals(list3,ToFrac.tofrac("5","5/6"));
Assert.assertEquals(list4,ToFrac.tofrac("1/2","2/3"));
}
Operator
public void testOperater() throws Exception{
String str1="1+2÷3=";
String str2="3+4*2+2/3=";
String str3="3-3/4-1*2÷3=";
String str4="2+1/4+1/3*3-3/4÷1/2=";
String str5="3*5=";
Assert.assertEquals(new ArrayList(Arrays.asList('+', '÷')),Calculate.Operator(str1));
Assert.assertEquals(new ArrayList(Arrays.asList('+', '*','+')),Calculate.Operator(str2));
Assert.assertEquals(new ArrayList(Arrays.asList('-', '-','*','÷')),Calculate.Operator(str3));
Assert.assertEquals(new ArrayList(Arrays.asList('+', '+','*','-','÷')),Calculate.Operator(str4));
Assert.assertEquals(new ArrayList(Arrays.asList('*')),Calculate.Operator(str5));
}
Figure
public void testFigure() throws Exception{
String str1="1+2÷3=";
String str2="3+4*2+2/3=";
String str3="3-3/4-1*2÷3=";
String str4="2+1/4+1/3*3-3/4÷1/2=";
String str5="3*5=";
Assert.assertEquals(new ArrayList(Arrays.asList("1","2","3")),Calculate.Figure(str1));
Assert.assertEquals(new ArrayList(Arrays.asList("3","4","2","2/3")),Calculate.Figure(str2));
Assert.assertEquals(new ArrayList(Arrays.asList("3","3/4","1","2","3")),Calculate.Figure(str3));
Assert.assertEquals(new ArrayList(Arrays.asList("2","1/4","1/3","3","3/4","1/2")),Calculate.Figure(str4));
Assert.assertEquals(new ArrayList(Arrays.asList("3","5")),Calculate.Figure(str5));
}
NewCalculate
public void testnewcalculate() throws Exception{
String str1="(1+2)÷3=";
String str2="3+4*2+2/3=";
String str3="3-(3/4-1)*2÷3=";
String str4="2+(1/4+1/3)*3-3/4÷1/2=";
String str5="3*5=";
Assert.assertEquals("1", Simplify.gcd(NewCalculate.newcalculate(str1)));
Assert.assertEquals("35/3", Simplify.gcd(NewCalculate.newcalculate(str2)));
Assert.assertEquals("19/6", Simplify.gcd(NewCalculate.newcalculate(str3)));
Assert.assertEquals("9/4",Simplify.gcd(NewCalculate.newcalculate(str4)));
Assert.assertEquals("15", Simplify.gcd(NewCalculate.newcalculate(str5)));
}
项目小结
此次个人项目我花了较多时间完成,虽然算法原理不难,但真正实现起来总是会出这样那样的错误,使得编程进度变慢。本科阶段学习数据结构时也没有多加练习,只是看着懂了就觉得掌握了,导致动手能力弱。我花了大部分时间在生成表达式以及表达式计算上,期间也参考了其他人的解题方法,当我以为大功告成时,又发现了一些空指针异常以及数组越界异常,然后又调试好久才找出错误,究其原因是我对问题可能出现的情况没有考虑完全,而这些小错误在我们开发时会被经常忽略。
通过这个项目,我也学习到了基本单元测试方法,使用了Junit。《构建之法》中说道:
软件的很多错误都来源于程序员对模块功能的误解、疏忽或不了解模块的变化,单元测试能使得模块质量能得到稳定、量化的保证。
所以单元测试及其他测试技术的学习非常有意义。但由于时间限制,我没能深入学习,在测试用例设计、覆盖率等方面都有待改进。总体来说本次项目还有很多需要改进和优化的地方,例如对程序进行效能分析、扩展程序功能等。