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

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

PSP表格

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划3030
· Estimate· 估计这个任务需要多少时间600700
Development开发560700
· Analysis· 需求分析 (包括学习新技术)4030
· Design Spec· 生成设计文档4030
· Design Review· 设计复审 (和同事审核设计文档)1020
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)2020
· Design· 具体设计6080
· Coding· 具体编码300420
· Code Review· 代码复审6040
· Test· 测试(自我测试,修改代码,提交修改)3060
Reporting报告100120
· Test Report· 测试报告4040
· Size Measurement· 计算工作量2020
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划4060
合计660820

#项目需求

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

  • 参与运算的操作数(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类,该类主要是实现控制台取得用户输入的题目数量和用户的计算结果。才外,该类实现结果的比对与分数的统计。
    系统流程如下:
    yCOuGKG.png

项目中的类

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

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

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

  • 成员变量
变量名类型备注
IsFuHaoboolean
upint在分数情况下,小于分母
downint在整数情况下,值为1
FuHaochar+-*÷
  • 成员方法
方法名参数返回值功能
GetNumvoidint获取100以内随机数
GetRandom_downRandom_upint获取随机分子
GetRandom_upvoidint获取随机分母

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

  • 成员变量
变量名类型备注
operationNumsOperationNum[]
operatorchar[]+ - * ÷
  • 成员方法
方法名参数返回值功能
CreateOperationvoidvoid填充operationNums[]
OperationConstructionvoidvoid执行CreateOperation
GetOperationvoidOperationNum[]获取生成的运算时operationNums[]

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

  • 常量
常量类型备注
priorityArrayint[][]运算符优先级矩阵
  • 数据结构
数据结构类型备注
stack_postOperationNum用于获得后缀式
stack_calculateOperationNum用于获得计算结果
operationNums_postOperationNum用于存放后缀式
  • 成员方法
方法名参数返回值功能
InToPostOperationNum[]OperationNum[]中缀式变后缀式
OperationCalculateOperationNum[]OperationNum由后缀式得到最终结果
CalculateOperationNum,OperationNum,OperationNumOperationNum获取运算符符号,执行运算
addOperationNum,OperationNumOperationNum加法
subtractOperationNum,OperationNumOperationNum减法
multiplyOperationNum,OperationNumOperationNum乘法
divideOperationNum,OperationNumOperationNum除法
GCDint,intint获得两数的做大公约数
checkLocationcharint获取运算符在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之间。

GnkJAky.png

进击模式下,整数的取值范围为0~99,分子分母取值范围为1~9

swodqpR.png

在困难模式下,整数取值范围为0~99,分子分母取值范围1~99

A7fzIQ8.png

测试中出现的问题

因为运算符个数随机,出现了运算符为一个的情况。
DkeJppf.png
对于这个问题在随机运算符个数的时候,做了微调整。


    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 );
}

72Uac4H.png

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

@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();
}

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

项目总结

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

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

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

2017年9月27日 更新:

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

单纯出10000题:
9ZUxy6Y.png

单纯出100000题:
JECJOp8.png

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

出10000道题,并验证:
OvWazo7.png

出100000道题,并验证:
a1hZErk.png

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

转载于:https://www.cnblogs.com/wtting/p/7580819.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值