四则运算自动生成程序
github项目传送门:https://github.com/LuozhanH/calculateApp.git
在线地址demo:https://luozhanh.github.io/calculateApp/
项目成员:罗展宏,谢东洪
一、项目相关要求
实现一个自动生成小学四则运算题目的命令行程序,并且具有验证对错的功能。
需求分析:
-
使用 -n 参数控制生成题目的个数,例如:
Myapp.exe -n 10 // 将会生成10个题目
-
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10 // 将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
-
生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
-
生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
-
每道题目中出现的运算符个数不超过3个。
-
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
1. 四则运算题目1
2. 四则运算题目2
……
PS: 其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
-
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
1. 答案1
2. 答案2
-
程序应能支持一万道题目的生成。
-
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
二、PSP
PSP2.1
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 100 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 100 | 60 |
Development | 开发 | 710 | 860 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 180 |
· Design Spec | · 生成设计文档 | 60 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 120 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
· Design | · 具体设计 | 120 | 60 |
· Coding | · 具体编码 | 210 | 360 |
· Code Review | · 代码复审 | 40 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 30 |
Reporting | 报告 | 100 | 100 |
· Test Report | · 测试报告 | 40 | 40 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 850 | 1020 |
三、开发预备
在实现代码之前,先对整个程序做了大概的了解。这个程序的关键点,在于如何实现四则运算的保存和计算结果。
通过不断查阅资料,发现了一个做运算程序非常实用的算法——调度场算法和逆波兰表达式
首先,我们先简单地介绍一下这两个方法:
调度场算法:
该算法规则为:从左到右遍历中缀表达式的每个数字和符号,如果是数字就输出,即成为后缀表达式的一部分,若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先于加减),则栈顶元素依次出栈并输出,并将当前的符号进栈,一直到最终输出后缀表达式为止。
逆波兰表达式:
逆波兰的求值步骤:
- 初始化一个空堆栈
- 如果字符是一个操作数,把它压入栈
- 如果字符是一个操作符号,弹出两个操作数,执行恰当操作,然后把结果压入堆栈,如果不能够弹出两个操作数,那么后缀表达式的语法错误。
- 到后缀表达式末尾,从堆栈中弹出结果,若后缀表达式格式正确,那么堆栈应该为空。
四、设计程序流程
五、关键代码
- 将问题/答案下载成一个.txt文件,因为js没办法操作本地文件,所以我们将文件转URI再又用户决定是否下载
// 生成txt文件,并且可以输出下载 function download(filename, text) { var pom = document.createElement('a'); pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); pom.setAttribute('download', filename); if (document.createEvent) { var event = document.createEvent('MouseEvents'); event.initEvent('click', true, true); pom.dispatchEvent(event); } else { pom.click(); } }
- 产生分数以及化简分数
// 分数化简 (a为分子,b为分母,mf为最大公约数) function fenShu (a,b,mf) { if(mf==1) { // 不需要化简分式 if(a>b && b!=1) { // 分子比较大,则转化为真分式 return parseInt(a/b) + '’' + a%b + '/' + b; } else if(a<b) { // 分子小则保留输出 return a + '/' + b; } else { // 分子分母相等,则为一 return 1; } } else { // 需要化简分式 // 对x,y根据最大公因子进行化简 var x = a/mf; var y = b/mf; // 判断分母是否为1 if(y==1) { return x; } else { return fenShu(x,y, getMaxFactor(x, y)); // 递归化简 直到最后为最简式子 } } } // 最简分数式 function simpleFenshu (num) { num = num.toString(); if (num.indexOf('/') == -1) { return num; } else { var arr = num.split('/'); arr[0] = Number(arr[0]); arr[1] = Number(arr[1]); return fenShu(arr[0], arr[1], getMaxFactor(arr[0], arr[1])); } } // 分数产生器 function createFenshu () { // 为了不产生比较难运算的分数,所以随机分子分母设定范围在(1,30)之间 var son_a = GetRandomNum(1,30); var par_b = GetRandomNum(1,30); var son_c = GetRandomNum(1,30); var par_d = GetRandomNum(1,30); // 合并成为分数X/Y var X = son_a + "/" + par_b; var Y = son_c + "/" + par_d; // 取最大公约数 max_x = getMaxFactor(son_a,par_b); max_y = getMaxFactor(son_c,par_d); // 取得最终返回的最简分数表示 var N = fenshu(son_a,par_b,max_x); var M = fenshu(son_c,par_d,max_y); return N; }
- 插入括号(只有在计算符为2或者3的时候插入,这里只放了 3个数 的情况)
var m = GetRandomNum(1, 4); if (m == 1) { return "( " + a[0] + Arr[n] + a[1] + " )" + Arr[n1] + "( " + a[2] + Arr[n2] + a[3] + " )"; } else if(m==2){ return a[0] + Arr[n] + "( " + a[1] + Arr[n1] + a[2] + " )" + Arr[n2] + a[3]; } else if(m==3){ return "( " + a[0] + Arr[n] + a[1] + " )" + Arr[n1] + a[2] + Arr[n2] + a[3]; } else { return a[0] + Arr[n] + a[1] + Arr[n1] + "( " + a[2] + Arr[n2] + a[3] + " )"; }
- 调度场算法实现
//优先级判断 function getPrioraty(value){ switch(value){ case '+': case '-': return 1; case '*': case '÷': return 2; default: return 0; } }
//调度场的关键判断函数
if(isOperator(cur)){ if(cur == '('){ outputStack.push(cur); }else if(cur == ')'){ var po = outputStack.pop(); while(po != '(' && outputStack.length > 0){ outputQueue.push(po); po = outputStack.pop(); } if(po != '('){ throw "error: unmatched ()"; } }else{ while(prioraty(cur, outputStack[outputStack.length - 1]) && outputStack.length > 0){ outputQueue.push(outputStack.pop()); } outputStack.push(cur); } }else{ outputQueue.push(cur); } - 逆波兰函数
function evalRpn(rpnQueue){ var outputStack = []; while(rpnQueue.length > 0){ var cur = rpnQueue.shift(); if(!isOperator(cur)){ outputStack.push(cur); }else{ if(outputStack.length < 2){ throw "unvalid stack length"; } var sec = outputStack.pop(); var fir = outputStack.pop(); outputStack.push(getResult(fir, sec, cur)); } } if(outputStack.length != 1){ throw "unvalid expression"; }else{ return outputStack[0]; } }
六、测试运行
- 整体风格
- 10000道题目测试
- 在线答题测试
- 下载题库以及题库答案
七、项目小结
- 数据结构很重要:一开始编写代码的时候没有考虑到如何查重的问题,所以直接用数组来存储数据而没有建树,导致后来查重只能用结果来查询。
- 1 + 1 > 2:结对编程过程中遇见问题都是可以两个人一起讨论解决,而且都是线下,工作效率奇高,遇见问题又不用孤孤单单自己问度娘,思路方法都可以互相共享,真香!
- Js真香:上次作业最终我们两个都没有选择用Js实现,这次拿到需求分析完毕直接Js开撸(其实有先在网上看过Java跟Py的源码,发现用html+Js也可以实现才用的)