第二次编程作业
这个作业属于哪个课程 | 网工1934-软件工程 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
1.相关代码已经上传到GitHub
2.PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 150 | 90 |
· Analysis | · 需求分析 (包括学习新技术) | 200 | 240 |
· Design Spec | · 生成设计文档 | 20 | 10 |
· Design Review | · 设计复审 | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
· Design | · 具体设计 | 30 | 20 |
· Coding | · 具体编码 | 120 | 240 |
· Code Review | · 代码复审 | 20 | 10 |
· Test | · 测试(自我测试,修改代码,提交修改) | 40 | 80 |
Reporting | 报告 | 60 | 70 |
· Test Repor | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 10 |
· | · 合计 | 725 | 845 |
3. 模块接口的设计以及实现过程
1. 主要实现方法
- 主类: index入口,主要作用是接收命令行的参数,以及调用工具函数执行程序
- 工具类:包括生成运算式、计算运算式、读写文件,commander.js(命令行编辑工具,需使用 yarn add commander 或 npm i commander 安装)
- 单元测试:test方法,测试主函数以及工具函数是否可以正常使用
项目结构:
项目流程以及函数间的关系:
2. 关键函数的分析与实现
实现函数的需求分析以及解释:
- 将中缀表达式转化为逆波兰式(后缀表达式),代码如下:
// 运算符集合
const symbol = ['#', '(', '+', '-', '*', '÷', ')']
const symbolPriority = {
'#': 0,
'(': 1,
'+': 2,
'-': 2,
'*': 3,
'÷': 3,
')': 4
}
/**
*
* @param {*} char 符号字符
* @param {*} symArr 存储符号表达式
* @param {*} resArr 记录后缀表达式元素
* @returns
*/
const operaSymbol = (char, symArr, resArr) => {
let lastChar = symArr[symArr.length - 1]
let curChar
if (!lastChar) {
symArr.push(char)
return
}
// 如果遇到左括号则直接入栈
if (char === '(') {
symArr.push(char)
}
// 如果遇到右括号,则弹出栈内只到出现左括号为止
else if (char === ')') {
curChar = symArr.pop()
while (symArr && curChar != '(') {
resArr.push(curChar)
curChar = symArr.pop()
}
}
// 如果栈外操作符的优先级高于栈内的优先级则入栈
else if (symbolPriority[char] > symbolPriority[lastChar]) {
symArr.push(char)
}
// 如果栈外的操作符优先级低于或等于栈内的优先级,输出栈内的符号,并入栈外的符号
else if (symbolPriority[char] <= symbolPriority[lastChar]) {
while (lastChar && (symbolPriority[char] <= symbolPriority[lastChar])) {
curChar = symArr.pop()
resArr.push(curChar)
lastChar = symArr[symArr.length - 1]
}
// operaSymbol(char, symArr, resArr)
symArr.push(char)
} else {
symArr.push(char)
}
}
/**
* 中缀表达式转化为后缀表达式(逆波兰表达式)
* @param {*} str 中缀表达式字符串
* @returns
*/
const toSuffixExpre = (str) => {
const resArr = new Array() // 记录后缀表达式
const symArr = new Array() // 记录符号表达式
// 用于记录数字
let substr = ''
for (let i = 0, len = str.length; i < len; i++) {
// 判断是否是符号
if (symbol.includes(str[i])) {
if (substr.length) resArr.push(substr)
substr = ''
operaSymbol(str[i], symArr, resArr)
} else {
// 收集符号间的数字元素
substr += str[i]
}
}
if (substr !== '') resArr.push(substr)
// 如果符号栈中还有元素,需要再推入存储后缀表达式的栈中
while (symArr.length > 0) {
const curChar = symArr.pop()
resArr.push(curChar)
}
return resArr
}
- 逆波兰式计算,代码如下:
/**
* 逆波兰运算
* @param {*} expression 运算式
* @returns
*/
const evalRPN = (expression) => {
// 将运算式多余的空格删除
let str = formatExpression(expression)
// 将中缀运算式按后缀运算式推入栈中
let tokens = toSuffixExpre(str)
const stack = [];
const n = tokens.length;
for (let i = 0; i < n; i++) {
const token = tokens[i];
if (isNumber(token)) {
if (isFraction(token)) stack.push(token);
else stack.push(parseInt(token));
} else {
let num2 = stack.pop();
let num1 = stack.pop();
if (token === '+') {
// 整数直接运算,分数则需要用另外的方法计算
if (!isFraction(num1) && !isFraction(num2)) {
stack.push(num1 + num2);
} else {
stack.push(evalFraction(num1, num2, '+'))
}
} else if (token === '-') {
if (!compareNum(num1, num2)) return false
if (!isFraction(num1) && !isFraction(num2)) {
stack.push(num1 - num2);
} else {
stack.push(evalFraction(num1, num2, '-'))
}
} else if (token === '*') {
if (!isFraction(num1) && !isFraction(num2)) {
stack.push(num1 * num2);
} else {
stack.push(evalFraction(num1, num2, '*'))
}
} else if (token === '÷') {
if (num2 === 0) return false
stack.push(evalFraction(num1, num2, '÷'))
}
}
}
return stack.pop();
};
- 相关分数计算及转化,代码如下:
/**
* 判断是否为分数
* @param {*} token
* @returns
*/
const isFraction = (token) => {
return typeof token !== 'number' && (token.includes('/') || token.includes('`'))
}
/**
* 判断是否为带分数
* @param {*} fraction 分数
*/
const isMixNum = (fraction) => {
return typeof fraction !== 'number' && fraction.includes('`')
}
/**
* 将带分数化为假分数
* @param {*} fraction 带分数
* @returns
*/
const turnImFraction = (fraction) => {
const arr = fraction.split(/[\/`]/)
let integer = parseInt(arr[0])
let numerator = parseInt(arr[1])
let denominator = parseInt(arr[2])
return integer * denominator + numerator + '/' + denominator
}
/**
* 分数式化简
* @param {*} numerator 分子
* @param {*} denominator 分母
* @returns
*/
const formatFraction = (numerator, denominator) => {
// 如果分母为1,则直接返回分子
if (denominator === 1) return numerator + ''
// 获取整数部分
let integerPart = Math.floor(numerator / denominator)
// 判断是否为假分数,是则转化为带分数
if (integerPart) {
// 抽出带分数的整数部分
numerator -= integerPart * denominator
return integerPart + '`' + numerator + '/' + denominator
}
return numerator + '/' + denominator
}
/**
* 分数式运算
* @param {*} num1
* @param {*} num2
* @param {*} symbol 运算符
*/
const evalFraction = (num1, num2, symbol) => {
if (isMixNum(num1)) num1 = turnImFraction(num1)
if (isMixNum(num2)) num2 = turnImFraction(num2)
const stackNum1 = resolveImFraction(num1)
const stackNum2 = resolveImFraction(num2)
let numerator = 0 // 分子
let denominator = 0 // 分母
let maxFac = 0 // 最大公约数
let ans
switch (symbol) {
case '+':
numerator = stackNum1[0] * stackNum2[1] + stackNum2[0] * stackNum1[1]
denominator = stackNum1[1] * stackNum2[1]
maxFac = getMaxFactor(numerator, denominator)
ans = formatFraction(numerator / maxFac, denominator / maxFac)
break;
case '-':
numerator = stackNum1[0] * stackNum2[1] - stackNum2[0] * stackNum1[1]
denominator = stackNum1[1] * stackNum2[1]
maxFac = getMaxFactor(numerator, denominator)
ans = formatFraction(numerator / maxFac, denominator / maxFac)
break;
case '*':
numerator = stackNum1[0] * stackNum2[0]
denominator = stackNum1[1] * stackNum2[1]
maxFac = getMaxFactor(numerator, denominator)
ans = formatFraction(numerator / maxFac, denominator / maxFac)
break;
case '÷':
numerator = stackNum1[0] * stackNum2[1]
denominator = stackNum1[1] * stackNum2[0]
maxFac = getMaxFactor(numerator, denominator)
ans = formatFraction(numerator / maxFac, denominator / maxFac)
break;
default:
throw new Error("this symbol is not an operator!")
break;
}
return ans
}
/**
* 分解数字
* @param {*} num
* @returns
*/
const resolveImFraction = (num) => {
let stack = []
// 分解假分数,若为整数则分母设为1
if (isFraction(num)) {
let strArr = num.split(/[\/]/)
stack = strArr.map(val => {
return +val
})
} else {
stack[0] = parseInt(num)
stack[1] = 1
}
return stack
}
算法流程:
4. 性能分析
5. 项目运行
// 常用命令行
node index.js -n 20 // 生成20道题目
node index.js -r 20 // 数字范围为1~20
node index.js -e <exercisefile> -a <answerfile> // 对比两个文件得到正确和错误题号,文件名需加上拓展符
// 进入文件夹所在路径后,可使用以下命令行启动项目
node index.js -n 20 -r 20 // 生成20道题目,数字范围为1~20
- 运行结果:
生成运算式:
生成比对结果:
同时,该程序允许生成10000道以上的式子:
6.异常处理
代码展示
如果找不到文件路径,就打印相关错误并结束程序
测试结果:
输入非法字符:
源文件输入路径错误:
7. 单元测试:
cd进入test文件后node test.js 即可:
8. 项目小结:
- 林澍锴: 因为我和马炽阳同学都是加入工作室担任开发的,所以完成这个项目其实算是比较简单的,简单沟通和分工之后就开始了各自的模块开发。我负责的模块的是运算及文件读写,感觉其中逻辑比较复杂的地方是有关于分数的计算,可能花了比较多时间去理清楚可能发生的一系列情况,中缀转后缀以及逆波兰计算由于之前刷题遇到过所以不算很难。还有由于是第一次接触nodejs命令行插件,用的不是很熟悉,也花费了一些时间熟悉用法。这个项目我感觉还是很不错的,巩固了我对逆波兰算法的认识,以及对一些nodejs工具插件的使用,对我帮助很多
- 马炽阳:这次我和澍锴打算用js开发项目,但由于我个人是后台开发,所以花了一部分时间熟悉前端语法,并完成了运算式生成的模块,运算式生成其实更多的是关于字符串的处理,真假分数判断、真假分数转换、分数化简、 对括号的处理,都比较考验我的大局观,经过多轮测试,最后才完成了比较精确的生成方案,这个项目对我的考验其实很大,毕竟是第一次使用前端语言,但是完成的还是比较满意的。我相信这对我之后成为一名全栈工程师有很大的帮助,也很高兴有这个机会接触前端知识。