1.介绍
本程序是一个具有图形化界面的小学生四则运算生成的程序,作者是:余圣源,王树干。本程序使用JavaScript结合electron框架打包生成的一个前端exe可执行程序。
这里对electron框架使用不做介绍。github地址:https://github.com/yushengyuan123/caculation,
2.效能分析
整个过程性能消耗最大的应该是答案的生成,答案的生成需要中缀表达式转为后缀,再去处理后缀表达式得出答案,整个过程需要比较复杂的逻辑判断。
3.设计过程
由于是两人合作,项目主要分为两个分工,一个人负责文件操作和页面html的书写,另一个人负责书写与计算式子相关的业务逻辑,包括计算式的随机产生和结果生成。把所有与计算式子相关的函数进行封装,供给负责文件,页面书写的同学进行调用。基本关系图如下:

由于使用了图形界面,为了重复没有使用控制台指令。图形界面展示如下:

4.代码说明
这里只展示业务中最核心的代码。
随机产生计算式算法。
const _isNumber = require('../share/utils').isNumber
const getRandom = require('../share/utils').getRandom
const getResult = require('./stack')
const print = require('../share/utils').printf
const _ = require('underscore')
const operator = ['+', '-', '*', '÷']
const _toString = Object.prototype.toString
/**
* 随机获得一个运算符
* @returns {string}
*/
function getOperator() {
return operator[Math.floor(Math.random() * (3 + 1))]
}
/**
*
* @param qus 提交的答案
* @param ans 正确的答案
* @returns {*}
*/
function verifyAnswer(qus, ans) {
let statistic
if (_toString.call(qus) !== '[object Array]' || _toString.call(ans) !== '[object Array]') {
throw new Error('Please dont set the poison for my code')
}
return statistic
}
//判断运算式子结果是否大于0
const _positive = function (expression) {
return getResult(expression) >= 0
}
/**
* 生成答案
* @param qus 传入生成计算式子返回的数组
* @returns {['1', '2', ...]}
*/
const answer = function (qus) {
if (_toString.call(qus) !== '[object Array]') {
throw new Error(qus + 'is not a Array')
}
let answer = []
for (let i = 0; i < qus.length; i++) {
let temp = qus[i].split('=')[0]
temp = temp.substring(0, temp.length - 1)
answer.push(getResult(temp))
}
return answer
}
/**
* 生成计算表达式
* @param number 传入你要生成多少条式子的数量
* @returns {['1 + 1 =', ...]}
*/
const createExpression = function(number) {
if(!_isNumber(number)) {
throw new Error(`The ${number} is not a number type, please check again!`)
}
let result = []
let operands = null
let output = ''
let index = 0
while(index !== number) {
operands = Math.floor(Math.random()* 3 + 1)
switch(operands) {
case 1: {
output = `${getRandom()} ${getOperator()} ${getRandom()}`
if(_positive(output)) {
result.push(`${output} = `)
index++
}
break
}
case 2: {
output = `${getRandom()} ${getOperator()} ${getRandom()} ${getOperator()} ${getRandom()}`
if(_positive(output)) {
result.push(`${output} = `)
index++
}
break
}
case 3: {
output = `${getRandom()} ${getOperator()} ${getRandom()} ${getOperator()} ${getRandom()} ${getOperator()} ${getRandom()}`
if(_positive(output)) {
result.push(`${output} = `)
index++
}
break
}
default: {
throw new Error('operands is not in the range of 3')
}
}
}
return result
}
module.exports = {
createExpression: createExpression,
answer: answer
}
随机产生分数或者自然数
//判断是假分数还是带分数 const bandFraction = function (fraction) { let temp = fraction.split('/') let result = Number(temp[0]) / Number(temp[1]) if (result > 1) { return `${Math.floor(result)}^${Number(temp[0] - Number(Math.floor(result)) * temp[1] + 1)}/${temp[1]}` } else if (result === 1){ return `1` } else { return fraction } } //随机返回分数或者是整数 //Math.floor(Math.random()*(m-n+1)+n) const getRandom = function() { //随机决定是生成整数还是分数1表示整数,0表示分数 let isZ = Math.round(Math.random()) if(isZ) { return Math.floor(Math.random() * 9 + 1) } else { let Molecule = Math.ceil(Math.random() * 9 + 1) let Denominator = Math.ceil(Math.random() * (Molecule * 10 - 1 + 1) + 1) return bandFraction(`${Denominator}/${Molecule}`) } }
产生思路说明:getRandom函数是决定随机生成分数还是自然数,使用一个随机产生0和1的变量决定产生哪一个,0产生分数,1产生自然数。生成计算表达式的时候也使用一个产生1-3的随机变量,决定该计算式子的长度,1表示两个操作数一个计算符号,2表示三个操作数两个运算符,3以此类推。生成一条计算式子的每一个操作数都调用一次getRandom产生随机操作数。整体思路就是这样。
计算结果呈现代码
/** * 符号优先级比较 * @param operator_one * @param operator_two * @returns {boolean} */ const operatorRank = function (operator_one, operator_two) { if(operator_one === void 0) { throw new Error('you not have a expression') } if (operator_two === undefined) { return true } if (operator_one === '/' || operator_one === '*') { return !(operator_two === '/' || operator_two === '*'); } else if (operator_one === '+' || operator_one === '-'){ return operator_two === ')' || operator_two === '('; } else if (operator_two === ')' || operator_two === '(') { return false } } const changeFormat = function (array) { let freeback = array.slice(0) for (let i = 0; i < array.length; i++) { if (array[i] === '÷') { freeback[i] = '/' } if (array[i].length > 1 && array[i].indexOf('/') !== -1) { if (array[i].indexOf('^') !== -1) { let temp = freeback[i].split('/') let one = temp[0].split('^') freeback[i] = Number(one[0]) + Number(one[1] / temp[1]) } else { let temp = freeback[i].split('/') freeback[i] = temp[0] / temp[1] } } } return freeback } /** * 计算器 * @param expressionArray * @returns {[]} */ const counter = function (expressionArray) { expressionArray = changeFormat(expressionArray.split(' ')) let outStack = [] let operatorStack = [] for (let i = 0; i < expressionArray.length; i++) { if (typeof Number(expressionArray[i]) == "number" && !isNaN(Number(expressionArray[i]))) { outStack.push(expressionArray[i]) } else if (expressionArray[i] === '(') { operatorStack.push(expressionArray[i]) } else if (expressionArray[i] === '+' || expressionArray[i] === '-' || expressionArray[i] === '*' || expressionArray[i] === '/') { if (operatorRank(expressionArray[i], operatorStack[operatorStack.length-1])) { operatorStack.push(expressionArray[i]) } else { outStack.push(operatorStack.pop()) while (!operatorRank(expressionArray[i], operatorStack[operatorStack.length-1])) { outStack.push(operatorStack.pop()) } operatorStack.push(expressionArray[i]) } } else if (expressionArray[i] === ')') { while (operatorStack[operatorStack.length-1] !== '(') { outStack.push(operatorStack.pop()) } if (operatorStack[operatorStack.length-1] === '(') { operatorStack.pop() } } if (i === expressionArray.length - 1) { while (operatorStack.length !== 0) { outStack.push(operatorStack.pop()) } } } return outStack } /** * 答案产生器 * @param suffix * @returns {[]} */ const getResult = function (suffix) { suffix = counter(suffix) let resultStack = [] for (let i = 0; i < suffix.length; i++) { if (typeof Number(suffix[i]) == "number" && !isNaN(Number(suffix[i]))) { resultStack.push(Number(suffix[i])) } else { switch (suffix[i]) { case '+': { resultStack.push(Number(resultStack.pop()) + Number(resultStack.pop())) break } case '-': { let reduce = Number(resultStack.pop()) let beReduce = Number(resultStack.pop()) resultStack.push(beReduce - reduce) break } case '*': { resultStack.push(Number(resultStack.pop()) * Number(resultStack.pop())) break } case '/': { let reduce = Number(resultStack.pop()) let beReduce = Number(resultStack.pop()) resultStack.push(beReduce / reduce) break } default: { throw new Error('illegal symbol ') } } } } return resultStack[0] } module.exports = getResult
答案产生思路如下:
最开始为了方便采用了eval()方法,它可以把一条计算式子进行解析成计算书结果,但是他有几个缺陷,第一可以通过它进行代码注入,十分危险。第二如果计算式子有分数,因为分数本质上是一个有优先级运行的除法式子,直接使用eval他无法识别你的是分数还是表示一个简单的除法。第三,太无脑了,显得没什么技术性。
考虑到了这几点因素,决定放弃这个方法的使用,虽然他十分方便,但是有很多弊端。最后自己写了一个计算器,就是把一个中缀表达式转成后缀表达式,利用后缀表达式在进行计算。最后得出结果,上面的代码就是把中缀表达式转成后缀表达式最后处理后缀表达式计算结果。这里不介绍中缀转后缀的思路过程。
5.测试运行
随机生成10题测试结果如图:

其中题目中的^代表带分数,我手动做了10题,其中有三题随便写了个答案看系统是否能够自动校验。发现检验结果是正确的。
提交题目的时候对应就会产生一份问题文件和答案文件和统计结果的输出,如下列截图:

其中三份文件截图如下:



最后的统计结果和界面题目的完成情况一致,正确7题,错误3题。
6.PSP表格记录
|
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
|
Planning |
计划 |
1440 |
1500 |
|
· Estimate |
· 估计这个任务需要多少时间 |
1440 |
1500 |
|
Development |
开发 |
1000 |
1100 |
|
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
40 |
|
· Design Spec |
· 生成设计文档 |
0 |
0 |
|
· Design Review |
· 设计复审 (和同事审核设计文档) |
60 |
80 |
|
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
60 |
60 |
|
· Design |
· 具体设计 |
60 |
60 |
|
· Coding |
· 具体编码 |
1000 |
1100 |
|
· Code Review |
· 代码复审 |
200 |
250 |
|
· Test |
· 测试(自我测试,修改代码,提交修改) |
200 |
250 |
|
Reporting |
报告 |
0 |
0 |
|
· Test Report |
· 测试报告 |
0 |
0 |
|
· Size Measurement |
· 计算工作量 |
1400 |
1200 |
|
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
60 |
60 |
|
合计 |
|
1400 |
1500 |
7.项目总结
这次项目再一次熟悉了electron框架打包的使用,整个项目我觉得最有挑战的地方的就是那个中缀表达式转后缀表达式的算法书写,很久之前尝试过用c语言实现,现在采用了js实现,比c实现简单一些。
浙公网安备 33010602011771号