一. github 地址
https://github.com/Chendabaiiii/expression-generator
项目合作者:郑秀欢 3218005084
, 陈锐基 3118005044
二.项目PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
Estimate | 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 1310 | |
Analysis | 需求分析 (包括学习新技术) | 60 | |
Design Spec | 生成设计文档 | 0 | |
Design Review | 设计复审 (和同事审核设计文档) | 0 | |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | |
Design | 具体设计 | 180 | |
Coding | 具体编码 | 1000 | |
Code Review | 代码复审 | 30 | |
Test | 测试(自我测试,修改代码,提交修改) | 30 | |
Reporting | 报告 | 120 | |
Test Report | 测试报告 | 60 | |
Size Measurement | 计算工作量 | 30 | |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | |
总计 | 1460 |
三.耗能测试
我们对代码的某些功能进行数量级耗能测试,其对应的图表如下
程序中消耗最大的函数:
/** * @description: 题目数组遍历转换为题目格式(string[]) * @param {Array[]} questionArr 题目数组 * @return: 转为固定格式的题目字符串数组 例如:1. a + b + c = */ let questionsToStr = questionArr => questionArr.map((expression, index) => { let str = expression.map(item => (typeof item === 'object') ? item.toStr() : item); str.unshift(`${index + 1}. `); return str.join('').concat(' = '); })
四. 设计实现过程
1. 技术栈
考虑到组队成员都是前端选手,又考虑到需要操作文件,但是由于浏览器没有操作文件的能力,最终决定用基于node
和前端三剑客的Electron
技术开发桌面程序。
2.代码组织
由于Electron
项目需要主进程和渲染进程(即展示页),并且到开发过程中需要函数可能较多,于是决定在根目录下的Utils
放功能函数,并用ES6模块化
进行模块管理。
1) 项目目录
App ├── node_modules // 依赖包 ├── index.html // 主页面 ├── main.js // 主进程 ├── renderer // 渲染进程(即展示页) │ ├── index.css │ └── index.mjs ├── Class // 类 │ ├── Operator.mjs // 操作符类 │ └── Operands.mjs // 操作数类 ├── Uitls // 存放功能函数 │ ├── brackets.mjs // 与括号相关的方法 │ ├── calculate.mjs // 与计算相关的方法 │ ├── questions.mjs // 与题目相关的方法 │ └── file.mjs // 关于文件读写的方法 │ └── index.mjs // 公共方法 ├── package.json // webpack 配置 ├── package-lock.json // webpack 配置 ├── .gitignore // github 推送忽略配置 ├── .babelrc // es6 babel 配置文件 ├── Answers.txt // 生成题目时的答案文件 ├── Exercises.txt // 生成题目时的题目文件 ├── Grade.txt // 校对答案时的结果文件 └── preload.js // 主页面
2)图形界面
在本次项目中,我们选择了图形界面的形式来实现一个四则运算表达式生成器
基本图形界面
校验错误
校验成功
3) 类的封装
①由于有分数和整数以及随机性,我们决定将操作数写为一个类,具有分子、分母和真值等属性,以及转换为字符串的方法,可以接收分子、分母和操作数范围生成实例;
②由于操作符具有优先级和随机性,我们也将操作符写为一个类,具有优先级以及转字符串的方法
将它们写成类的初衷是为了方便进行数组操作和转换操作。
Operands
操作数类
import { gcd, randomNum } from '../Utils/index.mjs'; import { rangeObj } from '../renderer/index.mjs' // 操作数类 export default class Operands { constructor({ range = rangeObj.range, // range 是范围 canBeZero = true, // canBeZero 代表该数字是否可以为0,由于存在作为除数的可能性不能为0 denominator = null, // 分母 1 numerator = null // 分子 0 }) { this.range = range; // 生成范围 this.denominator = denominator !== null ? Number(denominator) : randomNum(1, this.range - 1); // 分母 this.numerator = numerator !== null ? Number(numerator) : randomNum(canBeZero ? 0 : 1, this.denominator * this.range - 1); // 分子 this.value = this.numerator / this.denominator; // 数值 } // 转换为 a'b/c 格式的字符串 toStr() { this.absDen = Math.abs(this.denominator); // 取绝对值 1 this.absNum = Math.abs(this.numerator); // 取绝对值 0 this.isNegative = this.denominator * this.numerator < 0 ? '-' : ''; // 是否是负数 '' let integer = Math.floor(this.absNum / this.absDen); // 假分数前面的整数 0 let numerator = this.absNum % this.absDen; // 分子 0 let denominator = this.absDen; // 分母 1 if (numerator === 0) return `${this.absNum / denominator}`; // 是否整除 let gcdNum = gcd(numerator, denominator); // 求最大公约数 return `${this.isNegative}${integer === 0 ? '' : `${integer}'`}${numerator / gcdNum}/${denominator / gcdNum}`; } }
Operator
操作符类
import { randomNum } from '../Utils/index.mjs'; // 运算符类 export default class Operator { constructor(operator = ['+', '-', '×', '÷'][randomNum(0, 3)]) { this.operator = operator; // 操作符,默认是随机生成,也可以传入生成 this.value = this.getValue(); } // 计算运算符优先级 getValue() { switch (this.operator) { case "+": return 1; case "-": return 1; case "×": return 2; case "÷": return 2; default: // 不存在该运算符 return 0; } } // 转为字符串 toStr() { return ` ${this.operator} `; } }
4) 函数关系
五. 代码说明
主要函数:
generateQuestions
: 生成题目的主函数
/** * @description: 生成题目的函数 * @param {number} total 题目个数 * @param {number} range 参数范围 * @return: ['表达式1','表达式2'...] */ export let generateQuestions = (total, range) => { let questionArr = []; // 题目数组 let canBeZero = true; // 操作数是否可以为0 // 生成 total 个题目 for (let i = 0; i < total; i++) { let operandNum = randomNum(2, 4); // 2-4个操作数 let operatorNum = operandNum - 1; // 1-3个操作符 let expArr = []; // 表达式数组 for (let j = 0; j < operandNum; j++) { let operands = new Operands({ range, canBeZero }); expArr.push(operands); while (calculateExp(expArr).value < 0) { operands = new Operands({ range, canBeZero }); expArr.pop(); expArr.push(operands); } if (j !== operatorNum) { let operator = new Operator(); // 随机生成操作符 canBeZero = (operator.operator === '÷') ? false : true; // 如果操作符是 ÷ ,那么下一个生成数不能为 0 expArr.push(operator); } } questionArr.push(expArr); } // 给数组中每一条表达式插入括号 let insertBracketsArr = insertBrackets(questionArr); // 题目转为写入文件的字符串格式(无转后缀) let strQuestionsArr = questionsToStr(insertBracketsArr); // 转为写入文件格式的答案数组 let answers = insertBracketsArr.map((exp, index) => `${index+1}. ${calculateExp(exp).toStr()}`); writeFile('Exercises.txt', strQuestionsArr.join('\n')); writeFile('Answers.txt', answers.join('\n')); }
analyzeQuestions
: 分析题目文件并生成答案,与自己的答案文件进行比对生成 Grade.txt
/** * @description: 分析题目文件并生成答案,与自己的答案文件进行比对生成 Grade.txt * @param {string} exercisefile 题目文件的路径 * @param {string} answerfile 答案文件的路径 */ export let analyzeQuestions = (exercisefile, answerfile) => { // 读取题目文件 readFileToArr(exercisefile, (strQuestionsArr) => { // 解析每一个题目,并得到一个嵌套答案数组[[],[],[]] let realAnswersArr = strQuestionsArr.map(item => { let expArr = []; // 去除开头的 '1. '和结尾的 ' = ' item = item.substring(item.indexOf(".") + 1, item.indexOf(" = ")).trim(); expArr = item.split(""); // 字符串先转为数组,为了在括号旁边插入空格 // 括号旁边插入空格 for (let i = 0; i < expArr.length; i++) { if (expArr[i] === '(') { expArr.splice(i++ + 1, 0, " "); } else if (expArr[i] === ')') { expArr.splice(i++, 0, " "); } } // 通过空格隔开操作数、操作符与括号 expArr = expArr.join("").split(" "); // 将字符串表达式转为对应可运算的类 let answer = expArr.map(item => { if (["+", "×", "÷", "-"].indexOf(item) >= 0) { return new Operator(item); // 操作符 } else if ("(" === item || ")" === item) { return item; // 括号 } else { // 操作数,则将操作数的带分数的整数部分、分母、分子分离,并返回操作数实例 let element = item.split(/'|\//).map(item => parseInt(item)); switch (element.length) { case 1: // 操作数是整数 return new Operands({ denominator: 1, // 分母 numerator: element[0] // 分子 }); case 2: // 操作数不是带分数的分数 return new Operands({ denominator: element[1], numerator: element[0] }); case 3: // 操作数是带分数 return new Operands({ denominator: element[2], numerator: element[0] * element[2] + element[1] }); } } }) return calculateExp(answer) }) // 将答案文件与标准答案进行比对 compareAnswers(answerfile, realAnswersArr); }); }
calculateExp
: 计算表达式的值
/** * @description: 计算表达式的值 * @param {Object[]} expression 表达式 * @return: Oprands 答案实例 */ export let calculateExp = (expression) => { // 将中缀表达式转为后缀表达式 let temp = []; // 临时存放 let suffix = []; // 存放后缀表达式 expression.forEach(item => { if (item instanceof Operands) { suffix.push(item) // 遇到操作数,压入 suffix } else if (item === '(') { temp.push(item); // 遇到左括号,压入 temp } else if (item === ')') { // 遇到右括号 while (temp[temp.length - 1] !== '(') { suffix.push(temp.pop()); } temp.pop(); // 弹出左括号 } else if (item instanceof Operator) { // 运算符 // 如果栈顶是运算符,且栈顶运算符的优先级大于或等于该运算符 while (temp.length !== 0 && temp[temp.length - 1] instanceof Operator && temp[temp.length - 1].value >= item.value) { suffix.push(temp.pop()); } // 是空栈或者栈顶是左括号亦或是栈顶优先级低,则直接入栈到 temp temp.push(item); } }); while (temp.length !== 0) { suffix.push(temp.pop()); } // 以下过程将后缀表达式计算成答案并转为 Oprands 实例 const { addOperands, subOperands, multOperands, divOperands } = Arithmetic; // 四则运算方法 let answerStack = []; // 存放运算结果 suffix.forEach(item => { if (item instanceof Operands) { answerStack.push(item); // 如果是操作数则推入 } else { // 是操作符则弹出最顶出的两个操作数进行运算 let b = answerStack.pop(); let a = answerStack.pop(); let result = null; switch (item.operator) { case '+': result = addOperands(a, b); break; case '-': result = subOperands(a, b); break; case '×': result = multOperands(a, b); break; case '÷': result = divOperands(a, b); break; default: break; } answerStack.push(result); } }) return answerStack.pop(); }
insertBrackets
: 给问题数组的每个表达式插入括号的主要函数
/** * @description: 给问题数组的每个表达式插入括号的主要函数 * @param {Array[]} questionArr 表达式未插入括号的问题数组 * @return: 表达式插入括号后的问题数组 */ export let insertBrackets = (questionArr) => { return questionArr.map((item, index) => { let bracketsNum = 0; // 括号对的数目 switch (item.length) { case 5: bracketsNum = randomNum(0, 1); // 3个操作数则最多1对括号 break; case 7: bracketsNum = randomNum(0, 2); // 4个操作数则最多2对括号 break; default: break; } // 将原来 item 项(即一个表达式)随机插入 bracketsNum 对括号 let newItem = item; while (bracketsNum--) { newItem = randomInsertBrackets(newItem); } return newItem; }); }
六、测试运行
-
先生成100个题目
-
上传两个文件进行校对
-
校对结果如下
-
修改正确答案
-
再次进行校对
从图例可知,修改过答案的题号已被统计出来
七、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 15 |
Estimate | 估计这个任务需要多少时间 | 30 | 15 |
Development | 开发 | 1310 | 1410 |
Analysis | 需求分析 (包括学习新技术) | 60 | 100 |
Design Spec | 生成设计文档 | 0 | 0 |
Design Review | 设计复审 (和同事审核设计文档) | 0 | 0 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 30 |
Design | 具体设计 | 180 | 210 |
Coding | 具体编码 | 1000 | 1020 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 20 |
Reporting | 报告 | 120 | 145 |
Test Report | 测试报告 | 60 | 120 |
Size Measurement | 计算工作量 | 30 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 15 |
总计 | 1460 | 1570 |
八、项目小结
我们第一次使用 Electron 技术进行开发,期间需要用到 node.js 的相关知识,由于个人比较习惯使用es6语法,但在node环境下本身不太支持es6语法,一开始在构建项目目录和配置支持es6模块化的时候耗费了一些时间。在两个人开发的项目中,由于经常需要交流沟通来开发模块,所以两个人都需要对该项目使用的技术栈有基本的了解,当其中有一个对该项目需要用到的技术比较陌生的时候,就要尽快去了解,才能够赶上项目的进度。
在本次项目中,我们遇到的一个比较难找的bug,就是在进行插入括号的时候,会导致本来不为0的子表达式被括号括起来之后值为0,从而形成了 a ÷ 0 = ∞
的子表达式结果,在求最大公约数时进入了死循环,导致项目崩溃。后来在一步步排查中,才攻破了这个bug。但目前由于对去除重复的功能,我们没有更好的方法,因为暴力方法效率低耗能大,所以我们没有进行对它的开发。
结对共同感受:Code Review
的次数明显增多,能够减少很多bug的产生,同时也能学习到对方的某些骚操作和解决方法的思路。简而言之,男女搭配,干活不累。
秀欢闪光点:学习能力强,有责任感,头脑清晰,会主动地进行 Code Review
,善于交流 。
锐基闪光点:打码贼六,让我学到了很多新知识以及许多骚操作。
来源:https://www.cnblogs.com/Chendabaiiii/p/12596303.html