结对项目(node-JS)张文俊&简蕙兰

结对项目:四则运算题目生成器

基本信息

github地址: https://github.com/sliyoxn/questionGenerator

在线预览:https://sliyoxn.github.io/questionGenerator/

作者:软件工程18(1)班 张文俊3218004986 && 简蕙兰3218004992 (SakuraSnow && Maxwell_Who)

Tip: 打开抽屉后按ESC关闭

界面预览:

效能分析

下表为 计算x条表达式需要的时间(绿色线) 和 生成10000条题目需要的时间(蓝色线)。

算法的改进和页面性能的改进用时大约为8小时

算法改进

  1. 改用波兰表达式 && 正则进行求值
  2. 使用更多的随机选项生成更多元的表达式

页面性能改进

  1. 使用worker开启多线程, 压榨CPU并且防止主线程(页面)卡死
  2. 在计算大规模数据(比如生成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 += "\n";
	}
	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

由于我的实力不比大佬结对项目让我体验了一把去实习一般的感觉,在进行初步讨论与策划之后,大佬也初步意识到了我的水平,并涕泪俱下地(?)表示不打算放弃,逐给我推荐了很多相关资源,让我复习了一波(夯实了基础),看到大佬工整清晰的代码,我也感受了一次工作室级的代码规范。

随着项目进度的发展 我的作用逐渐趋近于打杂 ,我的工作逐渐多样化,体验到了从基本构思到实现到纠细节小错到激烈讨论的各种环节,也让我更加深刻地体会到编程的魅力以及程序员的伟大!

猜你喜欢

转载自www.cnblogs.com/maxwell-who/p/12695347.html