结对项目:四则运算题目生成器
基本信息
github地址: https://github.com/sliyoxn/questionGenerator
在线预览:https://sliyoxn.github.io/questionGenerator/
作者:软件工程18(1)班 张文俊3218004986 && 简蕙兰3218004992 (SakuraSnow && Maxwell_Who)
Tip: 打开抽屉后按ESC关闭
界面预览:
效能分析
下表为 计算x条表达式需要的时间(绿色线) 和 生成10000条题目需要的时间(蓝色线)。
算法的改进和页面性能的改进用时大约为8小时
算法改进
- 改用波兰表达式 && 正则进行求值
- 使用更多的随机选项生成更多元的表达式
页面性能改进
- 使用worker开启多线程, 压榨CPU并且防止主线程(页面)卡死
- 在计算大规模数据(比如生成1w+题目和判定大量题目对错时), 分批加载数据, 防止等待时间过长
使用波兰表达式前:
使用波兰表达式后
页面性能改进
使用worker的目的是,防止在加载1w+条数据时页面完全卡死(从数据看大概卡死2s)
使用分批加载的目的是, 能在500ms内能看到数据显示在页面上
代码实现
随机生成表达式
// from,to表示生成随机数的范围, count表示生成的题目数量,
// maxLoopCount是如果发现题目重复或者运算过程中出现负数允许的最大重试次数
// simpleExpressSet是已经有的题目的set
function generateTopic({from, to, count, maxLoopCount = count * 5, simpleExpressionSet} ) {
let str = "";
let answerArr = [];
let simpleExpression = "";
for (let i = 0; i < count; i++) {
// 生成单条题目
let expressionObj = getExpression(from, to);
let expression = expressionObj.expression;
// 生成的题目里有没有负子项
let hasNegativeNumber = expressionObj.hasNegativeNumber;
simpleExpression = expressionObj.simpleExpression;
let curLoopCount = 0;
let calRes = calEval(expression);
// 如果生成的有负子项就重新获取
while ((hasNegativeNumber === true || calRes.hasNegativeNumber || simpleExpressionSet.has(simpleExpression)) && curLoopCount < maxLoopCount) {
expressionObj = getExpression(from, to);
expression = expressionObj.expression;
simpleExpression = expressionObj.simpleExpression;
hasNegativeNumber = expressionObj.hasNegativeNumber;
calRes = calEval(expression);
curLoopCount ++;
}
// 防止死循环,设置最大重试次数
if (maxLoopCount <= curLoopCount) {
return {
text : str.slice(0, str.length - 1),
answer : answerArr,
warnMsg : "重试次数已达最大, 生成停止, 共计生成" + i + "题"
}
}
str += expression;
answerArr.push(calRes.val);
if (simpleExpression !== "") {
simpleExpressionSet.add(simpleExpression);
}
str += "
";
}
return {
text : str.slice(0, str.length - 1),
answer : answerArr,
simpleExpressionSet
}
}
// 获取单个表达式
function getExpression(from, to) {
let expression = '';
// 随机操作数
let leftVal = getRandomOperand(from, to);
// 随机生成符号
let operator = getRandomOperator();
// 随机生成操作符的个数
let operatorCount = getRandomOperatorCount();
// 随机判断是否使用首位括号
let useFirstIndexBracket = !!getRandom(0,1) && operatorCount >= 1;
let firstIndexBracketIndex = getRandom(1,operatorCount - 1);
let operandArr = [leftVal];
let operatorArr = [operator];
// 根据情况拼接字符串完成生成
if (useFirstIndexBracket && operatorCount >= 2) {
expression += `(${leftVal} ${operator} `;
operator = getRandomOperator();
operatorArr.push(operator);
expression += `${randomExpression(from, to, firstIndexBracketIndex - 1, operandArr, operatorArr)}) ${operator} `;
expression += `${randomExpression(from, to, operatorCount - firstIndexBracketIndex - 1, operandArr, operatorArr)}`
} else {
expression += `${leftVal} ${operator} `;
expression += `${randomExpression(from, to, operatorCount - 1, operandArr, operatorArr)}`
}
// 获取simpleExpression用于判定是否重复
let simpleExpression = getSimpleExpression(operandArr, operatorArr);
return {
expression,
simpleExpression
}
}
/**
* 递归生成表达式
* @param {Number} from
* @param {Number} to
* @param {Number} remain
* @param {Array} operandArr
* @param {Array} operatorArr
* @param {Object} hasNegativeNumberObj 一个含有hasNegativeNumber标识的对象
*/
function randomExpression(from, to, remain, operandArr, operatorArr, hasNegativeNumberObj) {
let leftVal = getRandomOperand(from, to);
let useBracket = !!getRandom(0,1);
operandArr.push(leftVal);
if (remain) {
let operator = getRandomOperator();
operatorArr.push(operator);
// rightExpress是一个表达式,可以计算是否为负数
let rightExpress = randomExpression(from, to, remain - 1, operandArr, operatorArr, hasNegativeNumberObj);
// 如果计算的是负数,就把标志位置为true
if (calEval(`${leftVal} ${operator} ${rightExpress}`).hasNegativeNumber) {
hasNegativeNumberObj.hasNegativeNumber = true;
}
if (useBracket) {
return `(${leftVal} ${operator} ${rightExpress})`;
} else {
return `${leftVal} ${operator} ${rightExpress}`
}
} else {
return leftVal;
}
}
计算表达式
function calEval(eval) {
// 中缀转后缀
let expression = transform(eval);
let operandStack = new Stack();
let array = expression.split(" ");
let hasNegativeNumber = false;
// 用栈进行后缀表达式的处理
while (array.length) {
let o = array.shift();
if (operandExp.test(o)) {
operandStack.push(o);
} else {
let a = operandStack.pop();
let b = operandStack.pop();
let res = Fraction.calculate(b, a, o);
if (res.value < 0 || res.value === Infinity) {
hasNegativeNumber = true;
return {
val: res.value === Infinity ? Infinity : "-99.99",
hasNegativeNumber
}
}
operandStack.push(res);
}
}
// 把结果丢回去
return {
val : operandStack.pop().toMixedString(),
hasNegativeNumber
}
};
/**
* 中缀转后缀
* @param {String} string
*/
function transform(string) {
let expression = "";
let operatorStack = new Stack();
while ((string = string.trim()) !== "") {
let operandTestRes = string.match(operandExp);
let operatorTestRes = string.match(operatorExp);
let isOperand = operandTestRes && (operandTestRes.index === 0);
let isOperator = operatorTestRes && (operatorTestRes.index === 0);
// 判断是操作数还是操作符
if (isOperand) {
let matchStr = operandTestRes[0];
expression += matchStr + " ";
string = string.slice(operandTestRes.index + matchStr.length);
} else if (isOperator) {
let operator = string[0];
let topOperator = null;
// 对不同操作符进行处理
switch (operator) {
case "+":
case "-":
case "*":
case "/":
topOperator = operatorStack.getTop();
if (topOperator) {
while (!operatorStack.isEmpty() && !comparePriority((topOperator = operatorStack.getTop()), operator)) {
expression += operatorStack.pop() + " ";
}
operatorStack.push(operator);
} else {
operatorStack.push(operator);
}
break;
case "(":
operatorStack.push(operator);
break;
case ")":
while ((topOperator = operatorStack.getTop()) !== "(") {
expression += operatorStack.pop() + " ";
}
operatorStack.pop();
break;
}
string = string.slice(1);
}
}
while (!operatorStack.isEmpty()) {
expression += operatorStack.pop() + " ";
}
expression = expression.trim();
return expression;
}
测试运行
测试calEval
这些计算表达式的测试用例覆盖了大部分的测试情况,所以可信的概率很大
在test/index.js中查看详情
测试生成题目
题目
答案
对生成的题目进行抽样复核,结果是正确的,所以可信概率较大
在test/index.js中查看详情
判卷
对一些答案进行修改,测试能识别错不一致,故可信的概率较大
PSP
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 60 |
· Estimate | 估计这个任务需要多少时间 | 5 | 15 |
Development | 开发 | 120 | 360 |
Analysis | 需求分析 (包括学习新技术) | 5 | 5 |
·Design Spec | 生成设计文档 | 20 | 30 |
·Design Review | 设计复审 (和同事审核设计文档) | 60 | 120 |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
·Design | 具体设计 | 60 | 40 |
·Coding | 具体编码 | 100 | 180 |
·Code Review | 代码复审 | 120 | 480 |
·Test | 测试(自我测试,修改代码,提交修改) | 30 | 20 |
Reporting | 报告 | 10 | 60 |
·Test Report | 测试报告 | 10 | 10 |
·Size Measurement | 计算工作量 | 10 | 10 |
·Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 | 575 | 1470 | |
项目小结
成败得失&&分享经验
这次结对项目耗时较长,主要是因为两人因为实力悬殊而需要较长的磨合时间,分工场面也比较混乱,因为其中一方实在太菜了而最后绝大部分工作都由另一方完成,菜鸡最终只实现了一两个小功能改了一些小bug……
但至少最终结果还非常不错。
结对感受
张文俊:
结对初期磨合时比较困难,经常因为自身的思维较为跳跃导致对方难以跟上,这时候就要停下来解释代码,导致思路容易被中断,好处就是对自己的代码有了更深刻的理解,经常解释到一半时觉得事情不大对劲,可以改一下实现or发现潜在bug。
项目后期习惯后就会有很多方便的地方,比如对方的英文比较好,省了我打开谷歌翻译找翻译的时间,还可以纠正我一些奇怪的拼写,知道对方会看我代码时,我会在潜意识里把代码写得更简洁易读,无形中提高了代码质量,还有在debug时,对方很耐心和我一起分析代码,让我在自己解释自己的代码时更容易发现问题(小黄鸭debug法),在项目后期,一起讨论优化and完善思路时,对方给了很多想法,比如开启多线程,批量更新,显示进度条,使用波兰表达式等。
简蕙兰:
内心对大佬充满歉意,同时非常感激大佬带我做项目作业,我发誓一定要好好学习不做吊车尾 orz
由于我的实力不比大佬结对项目让我体验了一把去实习一般的感觉,在进行初步讨论与策划之后,大佬也初步意识到了我的水平,并涕泪俱下地(?)表示不打算放弃,逐给我推荐了很多相关资源,让我复习了一波(夯实了基础),在一起实现各项功能的同时,大佬还会在我偶尔跟不上思路时开屏幕分享跟我细细讲解,看到大佬工整清晰的代码,我也感受了一次工作室级的代码规范。大佬提出了很多严谨的思路,比如不应出现负子式以及其它的一些优化问题。
随着项目进度的发展 我的作用逐渐趋近于打杂 ,我的工作逐渐多样化,体验到了从基本构思到实现,再到纠细节小错,最后到激烈讨论的各种环节,也让我更加深刻地体会到编程的魅力以及程序员的伟大!