高级软件工程2017第2次作业

项目Github地址:https://github.com/wtt1002/OperationTest.git

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
· Estimate · 估计这个任务需要多少时间 600 700
Development 开发 560 700
· Analysis · 需求分析 (包括学习新技术) 40 30
· Design Spec · 生成设计文档 40 30
· Design Review · 设计复审 (和同事审核设计文档) 10 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 20
· Design · 具体设计 60 80
· Coding · 具体编码 300 420
· Code Review · 代码复审 60 40
· Test · 测试(自我测试,修改代码,提交修改) 30 60
Reporting 报告 100 120
· Test Report · 测试报告 40 40
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 40 60
合计 660 820

项目需求


完成一个能自动生成小学四则运算题目的命令行 “软件”,满足以下需求:

  • 参与运算的操作数(operands)除了100以内的整数以外,还要支持真分数的四则运算,例如:1/6 + 1/8 = 7/24。操作数必须随机生成。(已完成)
  • 运算符(operators)为 +, −, ×, ÷ (如运算符个数固定,则不得小于3)运算符的种类和顺序必须随机生成。(已完成)
  • 要求能处理用户的输入,并判断对错,打分统计正确率。(已完成)
  • 使用 -n 参数控制生成题目的个数,例如执行下面命令将生成5个题目(已完成)
    (以C/C++/C#为例) calgen.exe -n 5
    (以python为例) python3 calgen.py -n 5

附加功能(算附加分)

  • 支持带括号的多元复合运算(正在调整)
  • 运算符个数随机生成(考虑小学生运算复杂度,范围在1~10)(已完成)

要求与说明

  • 【编程语言】不限
  • 【项目设计】分析并理解题目要求,独立完成整个项目,并将最新项目发布在Github上。
  • 【项目测试】使用单元测试对项目进行测试,并使用插件查看测试分支覆盖率等指标。
  • 【源代码管理】在项目实践过程中需要使用Github管理源代码,代码有进展即签入Github。签入记录不合理的项目会被助教抽查询问项目细节。
  • 【博客发布】按照要求发布博客,利用在构建之法中学习到的相关内容,结合个人项目的实践经历,撰写解决项目的心路历程与收获。博客与Github项目明显不符的作业将取消作业成绩。

需求分析

  • 所有参与运算的运算数取值范围整数(0~99),真分数要保证其为最简真分数,注意分母为0的情况,同时注意保证除数不为0。
  • 运算结果可能出现负数。
  • 运算符不少于3个,且随机,可暂定固定为3个,在四则运算中随机取3个。
  • 有输入与输出,可统计正确率。
  • 可控制生成题目的数目,数目由用户输入决定。

解题思路

刚开始看到四则运算,感觉不难,后来好好分析了一下,其实不简单,有很多问题。需要考虑。那就先把问题一步一步化简。
首先,把运算符的个数固定下来,就固定为3个,那么相应的操作数为4个。其次找出在这个项目里有几个重要的方法,一是运算式的生成,二是运算式结果的计算,三是运算结果比较,四是分数统计。
后期功能实现了扩展,可以随机运算符的数量,控制四则运算式的长度随机

需要注意的是:

(1)真分数的形成

(2)随机化生成操作数

(3)随机化生成操作码

(4)注意分母为0的情况

(5)注意正负号在分子还是分母上(后期添加的注意事项)

部分思路参考自:程序生成30道四则运算(包括整数和真分数) - 代码小逸 - 博客园 http://www.cnblogs.com/ly199553/p/5247658.html

设计实现

最开始的思路是把分数的“/”也做除法运算,即基本运算为+ - * /,设计到一半,就开始写代码了,结果遇到中间计算结果如何处理的瓶颈,计算结果如果是分数形式无法保证。我不得不将前面的思路前部推翻重来!真的是教训啊!
再次思考之后的思路是:

  1. 创建OperationNum类,把整数、分数、运算符都做包装在一个OperationNum对象里,这为后面进行堆栈操作提供了便利(中缀式变后缀式、计算结果)。OperationNum类中,IsFuHao用于判断当前对象是符号还是数字,对于整数分母置为1;对于分数,分母一定不为1。为了保证数据操作的一致性,做所有的计算操作都保证最终结果的分母为正数。此外此类还实现随机数、随机分母、分子的生成。
  2. 创建一个OperationConstruction类,该类的主要任务是实现运算四则运算式的生成。其结果为得到一个OperatonNum对象数组,初始化得到的OperationNums[]的大小。
    例如数组大小为7的时候,数组结构如下:
OperationNums[0] OperationNums[1] OperationNums[2] OperationNums[3] OperationNums[4] OperationNums[5] OperationNums[6]
操作数1 运算符1 操作数2 运算符2 操作数3 运算符3 操作数4
  1. 创建ResultDeal类,该类包含了一系列方法,最重要的方法有 InToPost(得到后缀式),OperationCalculate(得到计算结果)。后面在核心代码展示阶段还会详细说明。
  2. 创建UserIO类,该类主要是实现控制台取得用户输入的题目数量和用户的计算结果。才外,该类实现结果的比对与分数的统计。
    系统流程如下:

项目中的类

输入输出控制类(UserIO):获取用户输入的运算式个数,和运算结果,并对非法输入进行检测

  • 成员方法
方法名 参数 返回值 功能
OutPutOp OperationNum[] void 展现更人性化的输出效果
OutCome_Compare OperationNum,String boolean 判断用户的结果是否正确

操作数类(OperationNum):生成随机数,同时对分数进行相应的处理

  • 成员变量
变量名 类型 备注
IsFuHao boolean
up int 在分数情况下,小于分母
down int 在整数情况下,值为1
FuHao char +-*÷
  • 成员方法
方法名 参数 返回值 功能
GetNum void int 获取100以内随机数
GetRandom_down Random_up int 获取随机分子
GetRandom_up void int 获取随机分母

运算式生成类(OperationConstruction):控制生成合法的表达式

  • 成员变量
变量名 类型 备注
operationNums OperationNum[]
operator char[] + - * ÷
  • 成员方法
方法名 参数 返回值 功能
CreateOperation void void 填充operationNums[]
OperationConstruction void void 执行CreateOperation
GetOperation void OperationNum[] 获取生成的运算时operationNums[]

结果处理类(ResultDeal):对用户输入结果进行比较和统计

  • 常量
常量 类型 备注
priorityArray int[][] 运算符优先级矩阵
  • 数据结构
数据结构 类型 备注
stack_post OperationNum 用于获得后缀式
stack_calculate OperationNum 用于获得计算结果
operationNums_post OperationNum 用于存放后缀式
  • 成员方法
方法名 参数 返回值 功能
InToPost OperationNum[] OperationNum[] 中缀式变后缀式
OperationCalculate OperationNum[] OperationNum 由后缀式得到最终结果
Calculate OperationNum,OperationNum,OperationNum OperationNum 获取运算符符号,执行运算
add OperationNum,OperationNum OperationNum 加法
subtract OperationNum,OperationNum OperationNum 减法
multiply OperationNum,OperationNum OperationNum 乘法
divide OperationNum,OperationNum OperationNum 除法
GCD int,int int 获得两数的做大公约数
checkLocation char int 获取运算符在priorityArry的坐标

核心代码

  • 四则运算式的构造
    本工程里所有的操作数与运算符都被封装为OperationNum类型。
	public void CreateOperation()
	{
		Random random=new Random();
		{
			int ZhengOrFen;//随机决定是整数还是分数
			int FuHao;//随机化符号
		for(int i=0;i<Length_Operation;i=i+2)//取分数和符号三次
		{
			ZhengOrFen=Math.abs(random.nextInt()%2);
			if(ZhengOrFen==1)
			{
				OperationNum FenNum=new OperationNum();
				FenNum.down=FenNum.GetRandNum_down();
				FenNum.up=FenNum.GetRandNum_up(FenNum.down);
				int gcd=GCD(FenNum.up, FenNum.down);
				FenNum.down/=gcd;
				FenNum.up/=gcd;
				operationNums[i]=FenNum;			
			}
			else//0取整数
			{
				OperationNum ZhengNum=new OperationNum();
				ZhengNum.down=1;
				ZhengNum.up=ZhengNum.GetNum();
				operationNums[i]=ZhengNum;
			}
			if (i+1<Length_Operation) {
				 FuHao=Math.abs(random.nextInt()%4);
				 OperationNum FuNum=new OperationNum();
				 FuNum.operator=operator[FuHao];
				 FuNum.IsFuHao=true;
				 operationNums[i+1]=FuNum;
			}
		 }

	  }
	}

通过该方法,可以随机构造运算式,并且操作数与运算符是相同类型,为后面中缀式转后缀式及运算式结果的计算做好铺垫。CreateOperation()操作完成后,operationNums[]被填充起来。

  • 运算结果的计算
public OperationNum OperationCalculate(OperationNum opNums[]) {
		
		//先变为后缀式
		InToPost(opNums);
		OperationNum NumOne=new OperationNum();
		OperationNum NumTwo=new OperationNum();
		//清空堆栈
		stack_calculate.clear();
		for (int i = 0; i < operationNums_post.length; i++) {
			
			if (operationNums_post[i].IsFuHao) {//如果是符号出栈运算
				NumTwo=stack_calculate.pop();
				NumOne=stack_calculate.pop();
				OperationNum NewTemp=new OperationNum();
				NewTemp=Calculate(NumOne,NumTwo,operationNums_post[i] );
				
				//检查每次压栈的数字是否合法
				//if(NewTemp.down=0)
				stack_calculate.push(NewTemp);
				
			}
			else {//如果是数字,入栈
				//System.out.println("result70,准备入栈");
				stack_calculate.push(operationNums_post[i]);
			}
		}
		OperationNum  outComeNum=stack_calculate.pop();
		
		return outComeNum;
		
	}

该函数完成运算式结果的计算,返回计算结果,计算结果也为OperationNum类型。函数最开始调用InToPost(OperationNum opNums[]),InToPost函数的主要功能是实现中缀式转后缀式,别把结果存入一个OperationNum对象数组operationNums_post[ ]里面。得到后缀式后,再利用栈,再对后缀式进行计算。

  • 对于除法操作的特殊处理

由于除法的除数不能为零,并且为了处理的方便,我们把所有的负号都放在了分子上,可是直接进行除法操作,很可能把负号带到分母里面。故需要特殊处理一下。

public OperationNum divide(OperationNum NumOne,OperationNum NumTwo) {
		
		//如果分母为0,IsLegal置false,操作中止
		if (NumTwo.up==0) {
			IsLegal=false;
			return null;
		}
		OperationNum tmpNum=new OperationNum();
		tmpNum.up=NumOne.up*NumTwo.down;
		tmpNum.down=NumOne.down*NumTwo.up;
		if (tmpNum.down<0) {//若分母为负数
			tmpNum.up=tmpNum.up*(-1);
			tmpNum.down=tmpNum.down*(-1);
		}
		int gcd=GCD(tmpNum.up, tmpNum.down);
		tmpNum.up/=gcd;
		tmpNum.down/=gcd;
		return tmpNum;
		
	}
  • 用户输入计算结果的计算
	public boolean OutCome_Compare(OperationNum outCome,String outString) {
		
		String outComeString=new String();
          //如果计算结果为整数
		if (outCome.down==1) {
			outComeString=""+outCome.up;
		}
		else {//计算结果为分数
			outComeString=""+outCome.up;
			outComeString+="/";
			outComeString+=outCome.down;
		}
		
		if (outComeString.equals(outString)) {
			return true;
		}
		return false;
		
	}

用户输入存为String类型,计算结果也转换为String类型,方便比较。

测试运行

软件运行截图

较简单的模式下,随机数的取值在0~9之间。

进击模式下,整数的取值范围为099,分子分母取值范围为19

在困难模式下,整数取值范围为099,分子分母取值范围199

测试中出现的问题

因为运算符个数随机,出现了运算符为一个的情况。

对于这个问题在随机运算符个数的时候,做了微调整。


	Random random=new Random();
	//随机化符号的个数
	int randomLen_FuHao=Math.abs(random.nextInt()%10);//可能随机到0;
    //得到随机后的运算式长度
	int Length_Operation=randomLen_FuHao*2+1;

调整之后:

	Random random=new Random();
	//随机化符号的个数
	int randomLen_FuHao=Math.abs(random.nextInt()%9)+1;//随机范围为1~9
    //得到随机后的运算式长度
	int Length_Operation=randomLen_FuHao*2+1;

单元测试结果

测试运算式的构造

@Test
public void testConstruction()
{
	OperationConstruction operationConstruction=new OperationConstruction();
	OperationNum[] opNums=operationConstruction.GetOperation();
	int length=opNums.length;
	Assert.assertEquals(1, length%2);
	System.out.println("运算式长度:"+length );
}

结果处理 中缀式转后缀式 测试

@Test
public void testIntoPost() {
	
	ResultDeal resultDeal=new ResultDeal();
	//   1/2+4÷2/3*8
	OperationNum[] testNums=new OperationNum[7];
	testNums[0]=new OperationNum(false,1,2,' ');
	testNums[1]=new OperationNum(true,1,1,'+');
	testNums[2]=new OperationNum(false,4,1,' ');
	testNums[3]=new OperationNum(true,1,1,'÷');
	testNums[4]=new OperationNum(false,2,3,' ');
	testNums[5]=new OperationNum(true,1,1,'*');
	testNums[6]=new OperationNum(false,8,1,' ');
	
	OperationNum[] testNums_post=resultDeal.InToPost(testNums);
	
	for (int i = 0; i < testNums_post.length; i++) {
		if (testNums_post[i].Get_IsFuHao()) {
			System.out.print(testNums_post[i].Get_FuHao()+"  ");
		}
		else {
			if (testNums_post[i].Get_down()>1) {
				System.out.print(testNums_post[i].Get_up());
				System.out.print("/");
				System.out.print(testNums_post[i].Get_down()+"  ");
			}
			else {
				System.out.print(testNums_post[i].Get_up()+"  ");
			}
		}
	}
	System.out.println();
}


通过测试,暂时未发现不正确的输出。

项目总结

这次项目耗时较长,没想到会消耗这么长的时间,只要原因是在开始的设计思路出现了问题。最开始想直接用字符串存储我随机生成的运算表达式,用这种结构,前面还勉勉强强能写下去。直到写到运算处理,如果把“/”也看做除法,那我运算之后如何存为分数形式呢?想到这一块,才明白前面的设计都错了,贪图节省时间,实际却浪费了很多时间。后面,我就转换思路,我开始思考可不可以用一个类把整数、分数、操作符都包起来,后来这个方法行通了,并且扩展性还挺好,但是在存储消耗上会大一些。

完成项目的基本功能之后,我又实现了一个附加功能,可以随机化运算符的个数。在设计分数的运算时,我发现分子和分母取值很大没有意义,有悖于给小学生出题的设计初衷,于是将随机化生成的分子分母都小于10。为了实现随机化设计的分数都是真分数,我首先随机化生成分母,然后再把分母的随机化取值作为分子随机化的范围,这样就可以保证生成的分数一定是真分数了。在分子分母随机化过程中要剔除等于0的情况。如果分母为0,没有意义,去掉;如果分子为0,分母不为0,整个分数等于0,实际就是整数了。而在数据处理的规则里,对于整数,分母保证其为1,即:如果Get_up()方法得到的结果为1,则这个数为整数。

此外,这次项目我练习使用了Junit,对单元测试有了一个初步的了解,觉得它特别的方便。我之前调代码都是通过打印一些状态信息发现问题或者是断点调试,用了junit之后,发现之前的方法其实是很繁琐的。我之前没有接触过软件工程开发,对它的了解是从0开始的,还有许多软件性能测试的软件与方法,我现在还不太懂,但是我会一点点不断加入到我的软件开发过程中,争取把自己的项目做的更好。

2017年9月27日 更新:

今天和同学讨论我们所写程序的执行效率问题,于是我回实验室对我的代码做了一个简单的测试:
单纯出题目10000题耗时约0.554s,出100000题耗时约3.917秒。

单纯出10000题:

单纯出100000题:

如果对所出题进行判断去除掉会使分母为零的情况,并计算出运算结果,速度会稍微放慢一点,出10000题耗时约0.574s;出100000题约4.474s。整体效率较高。

出10000道题,并验证:

出100000道题,并验证:

经过测试,整个系统的出题效率比较高。

posted @ 2017-09-26 17:26  刘旭是一头小水猪  阅读(272)  评论(2编辑  收藏  举报