第三次作业:结对项目
结对项目:小学四则运算题目生成器
作业 GitHub 链接:小学四则运算题目生成器
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 计科 24 级 5 班 |
| 这个作业的要求在哪 | 结对项目:小学四则运算题目生成器 |
| 这个作业的目标 | 两人结对实现一个命令行四则运算题目生成器,支持题目生成、答案生成、去重、判题统计、单元测试和性能分析。 |
| 结对成员 | 3224004080 李蔓淇;3224004119 严翊郗 |
一、PSP 表格(预估)
开始编码前,我们将需求拆成表达式建模、命令行、测试和文档几个模块,并作出如下时间预估。
| PSP2.1 | 预估耗时(分钟) |
|---|---|
| Planning | |
| · Estimate(估计这个任务需要多少时间) | 25 |
| Development | |
| · Analysis(需求分析,包括学习新技术) | 50 |
| · Design Spec(生成设计文档) | 30 |
| · Design Review(设计复审) | 20 |
| · Coding Standard(代码规范) | 15 |
| · Design(具体设计) | 45 |
| · Coding(具体编码) | 150 |
| · Code Review(代码复审) | 30 |
| · Test(自我测试、修改代码、提交修改) | 100 |
| Reporting | |
| · Test Report(测试报告) | 30 |
| · Size Measurement(计算工作量) | 15 |
| · Postmortem & Process Improvement Plan | 30 |
| 合计 | 540 |
二、需求分析与总体设计
1. 需求中最需要注意的约束
本题不是简单随机拼接数字和符号。程序必须保证每个中间子表达式都合法,因此我们将下列限制放在“构造表达式节点”这一处统一检查:
- 自然数取值范围为
[0, r);分数分母小于r; - 减法只有在左子表达式的值不小于右子表达式时才允许生成;
- 除法要排除除数为 0,且商必须满足
0 < 商 < 1,即为真分数; - 每题随机使用 1~3 个运算符;
- 同一次生成中,允许对
+、×的左右孩子做交换后相同的表达式只能保留一次; - 生成模式输出
Exercises.txt与Answers.txt,判题模式输出Grade.txt。
2. 项目结构
项目采用 Node.js 标准库实现,不需要安装第三方依赖。核心代码集中在一个文件中,便于命令行作业提交和运行。
calculator/
├── Myapp.js # 程序入口、表达式模型、生成、解析与判题
├── tests/
│ └── test.js # 10 个自动化测试
├── README.md # 运行说明
├── 性能分析.svg # 性能柱状图
├── Exercises.txt # 生成题目示例
├── Answers.txt # 生成答案示例
└── Grade.txt # 判题结果示例
3. 模块关系
命令行 main(args)
├── 生成模式:generate(n, r)
│ └── Generator.generate()
│ └── Generator.expr() → combine() → Expr
│ ├── Rat:精确分数运算
│ ├── Expr.render():输出题目
│ └── Expr.canonical():去重
└── 判题模式:grade(exerciseFile, answerFile)
└── parse() → tokens() → Expr → Rat 比较
Rat 负责精确有理数计算;Expr 表示一棵表达式树;Generator 随机创建树;parse 用递归下降方式解析题目文件;grade 比较标准答案与用户答案并输出统计结果。
4. 生成流程
开始
│
▼
读取 -n 与 -r,检查参数
│
▼
随机选择 1~3 个运算符,并递归生成左右子树
│
▼
combine() 检查本次合并是否合法
├── 减法:左值 < 右值 → 舍弃
├── 除法:除数为 0 或商不是真分数 → 舍弃
└── 其余情况 → 建立 Expr 节点
│
▼
计算 canonical() 规范键,检查是否与已有题目重复
├── 重复 → 重新生成
└── 不重复 → 保存题目和答案
│
▼
达到 n 道题?──否──→ 继续生成
│是
▼
写入 Exercises.txt 与 Answers.txt
│
▼
结束
三、核心实现说明
1. 使用 BigInt 实现精确分数
JavaScript 的 Number 使用浮点数,直接计算 1/6 + 1/8 可能出现精度误差。我们自定义 Rat 类,分子和分母都使用 BigInt,构造时调用最大公约数函数约分,因此每一步的结果都是精确值。
const gcd = (a, b) => {
a = a < 0n ? -a : a;
b = b < 0n ? -b : b;
while (b) [a, b] = [b, a % b];
return a;
};
class Rat {
constructor(n, d = 1n) {
if (d === 0n) throw Error('分母不能为零');
if (d < 0n) [n, d] = [-n, -d];
const g = gcd(n, d);
this.n = n / g;
this.d = d / g;
}
add(x) { return new Rat(this.n * x.d + x.n * this.d, this.d * x.d); }
mul(x) { return new Rat(this.n * x.n, this.d * x.d); }
}
text() 方法会将 19/8 格式化成题目要求的 2’3/8,将 3/5 格式化成 3/5。
2. 统一检查减法和除法约束
所有二元运算都通过 combine 创建。这样,随机生成和判题解析共享同一套合法性规则,避免两个模块的判断不一致。
function combine(op, a, b) {
let v;
if (op === '+') v = a.value.add(b.value);
else if (op === '-') {
if (a.value.cmp(b.value) < 0) return null;
v = a.value.sub(b.value);
} else if (op === '×') {
v = a.value.mul(b.value);
} else {
if (b.value.cmp(zero) === 0) return null;
v = a.value.div(b.value);
if (v.cmp(zero) <= 0 || v.cmp(new Rat(1n)) >= 0) return null;
}
return new Expr(v, op, a, b);
}
这里的 null 表示当前随机组合不合法,生成器会重新尝试,而不是把不符合小学运算规则的题目写入文件。
3. 交换律去重,但不错误使用结合律
本题的难点是不能把 + 和 × 交换后的题目重复生成,同时又不能把不同结合顺序的表达式误认为重复。我们为每个表达式构造规范键:叶子节点由分子/分母组成;对于 + 与 × 节点,仅将两个孩子的规范键按字典序排序;对于 -、÷ 则保持左右顺序。
canonical() {
if (!this.op) return `n:${this.value.key()}`;
let a = this.left.canonical();
let b = this.right.canonical();
if ((this.op === '+' || this.op === '×') && b < a) [a, b] = [b, a];
return `${this.op}(${a},${b})`;
}
因此,3 + (2 + 1) 与 (1 + 2) + 3 会获得相同的规范键;而 (1 + 2) + 3 与 (3 + 2) + 1 的树形不同,规范键也不同,满足题目的要求。
4. 判题:递归下降解析器
判题不能通过字符串比较答案,而要重新计算题目表达式。parse 按“原子 → 乘除 → 加减”的优先级解析,并支持括号、×/*、÷// 以及直引号/弯引号的带分数输入。之后将解析值与答案文件中同编号答案比较,分别写入 Correct 和 Wrong。
四、程序运行与结果展示
1. 自动化测试
运行命令:
& $nodePath tests\test.js
输出显示 10 tests passed.,说明 10 个自动化测试均通过。

2. 生成题目与答案
运行命令:
& $nodePath Myapp.js -n 10 -r 10
Get-Content Exercises.txt -Encoding UTF8
Get-Content Answers.txt -Encoding UTF8
程序在当前目录生成 10 道题目及对应答案。Windows PowerShell 默认编码可能把 ×、÷、’ 显示为乱码,因此查看 UTF-8 文件时显式使用 -Encoding UTF8。

3. 判题功能
运行命令:
& $nodePath Myapp.js -e Exercises.txt -a Answers.txt
Get-Content Grade.txt -Encoding UTF8
程序读取题目和答案,输出每道题的正确/错误统计及编号。以刚刚生成的答案文件为输入时,预期结果为 Correct: 10 (...)、Wrong: 0 ()。

五、测试用例
除手工运行外,tests/test.js 使用 Node.js 内置 assert 编写了 10 个自动化测试,覆盖算法、边界和端到端判题场景。
| 序号 | 测试项 | 验证目标 | 预期结果 |
|---|---|---|---|
| 1 | 分数格式化 | 3/5 与 19/8 的输出格式 |
3/5、2’3/8 |
| 2 | 分数加法 | 1/6 + 1/8 的精确计算 |
7/24 |
| 3 | 运算优先级 | 1 + 2 × 3 |
7 |
| 4 | 括号 | (1 + 2) × 3 |
9 |
| 5 | 负数限制 | 1 - 2 不允许构造 |
返回 null |
| 6 | 除法真分数限制 | 4 ÷ 2 与 1 ÷ 2 |
前者拒绝,后者为 1/2 |
| 7 | 交换律去重 | 3+(2+1) 与 1+2+3 |
规范键相同 |
| 8 | 不使用结合律 | 1+2+3 与 3+2+1 |
规范键不同 |
| 9 | 批量生成约束 | 生成 100 道题 | 无重复,且运算符数不超过 3 |
| 10 | 判题报告 | 一题正确、一题错误 | Correct/Wrong 编号正确 |
这些测试分别覆盖了题目的核心计算规则、边界条件、去重规则与完整文件判题流程。测试通过并不只说明某一个算式正确,而是说明关键约束均有回归保护;后续修改代码后重新运行测试,也能及时发现破坏已有功能的问题。
六、效能分析与改进
1. 测试方法
题目要求支持一次生成 10000 道题目。我们使用 PowerShell 的 Measure-Command 测量从生成开始到文件写入完成的总耗时:
Measure-Command { & $nodePath Myapp.js -n 10000 -r 10 }

2. 优化思路
性能主要受随机表达式构造、分数约分和重复检测影响。对应的改进如下:
- 使用
BigInt和最大公约数进行精确约分,不使用浮点数,也不需要将分数反复转换为字符串参与运算。 - 每个表达式生成规范键后立即放入
Set,平均 O(1) 完成重复检测;不需要两两比较所有题目,避免 O(n²) 的重复判断。 - 将运算符数量限制为 3,表达式树规模有明确上界;每次不合法合并会立即丢弃,减少无效的后续处理。
本机测试中,生成 10000 道、范围为 10 的题目耗时约 0.13 秒,满足生成万题的功能要求。按照函数调用关系,Generator.generate() 是总控热点,其中最频繁执行的是 expr()、combine() 和 canonical():前者负责随机树构造,后两者分别负责合法性过滤和哈希去重。
3. 性能图
下面的柱状图记录了 10、100、1000、10000 道题目的测试结果,横轴为题目数量,纵轴为生成总耗时。
【图片占位:在此插入项目中的
性能分析.svg】
七、PSP 表格(实际)
完成项目后,实际耗时如下。编码、测试和整理运行截图的耗时比预估略高,主要因为我们花了额外时间处理 Windows PowerShell 的 UTF-8 显示问题,并补充了判题和去重边界测试。
| PSP2.1 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning | ||
| · Estimate(估计这个任务需要多少时间) | 25 | 20 |
| Development | ||
| · Analysis(需求分析,包括学习新技术) | 50 | 55 |
| · Design Spec(生成设计文档) | 30 | 35 |
| · Design Review(设计复审) | 20 | 25 |
| · Coding Standard(代码规范) | 15 | 15 |
| · Design(具体设计) | 45 | 50 |
| · Coding(具体编码) | 150 | 170 |
| · Code Review(代码复审) | 30 | 35 |
| · Test(自我测试、修改代码、提交修改) | 100 | 120 |
| Reporting | ||
| · Test Report(测试报告) | 30 | 35 |
| · Size Measurement(计算工作量) | 15 | 10 |
| · Postmortem & Process Improvement Plan | 30 | 35 |
| 合计 | 540 | 605 |
八、Git 提交记录
项目使用 Git 管理代码。主要提交记录如下:
| 提交 | 说明 |
|---|---|
a3d6576 |
Initial commit: 小学四则运算题目生成器 |
64daf9c |
feat: 添加小学四则运算题目生成器项目到 calculator 目录 |
2152edd |
chore: 删除无用文件1 |
提交记录体现了项目从初始化、功能整理到清理无关文件的过程。完整记录可在 GitHub 仓库的提交历史中查看。
九、项目小结与结对感受
1. 成果与不足
本次结对完成了题目生成、标准答案生成、真分数格式化、表达式去重、文件判题和性能测试等全部功能。最有价值的设计是把四则运算约束集中在 combine() 中,并通过表达式树规范键处理“只允许交换、不能随意结合”的去重规则。这样既避免了负数和不合法除法,也能将生成与判题使用同一套规则。
不足之处是项目刚开始运行时遇到了 Windows PowerShell 对 UTF-8 文件的默认显示编码问题。文件本身采用 UTF-8 写入是正确的,但直接 Get-Content 会显示乱码。之后我们明确在运行说明中加入 -Encoding UTF8,并在测试时验证了题目符号和带分数格式能正确显示。今后会在更早阶段把运行环境与编码兼容性加入测试清单。
2. 结对感受
李蔓淇认为,本次合作中最重要的是先把需求中的边界条件逐一写清楚,特别是“除法结果是真分数”和“去重只使用交换律”两条。严翊郗在命令行运行、Git 提交和问题定位方面推进较快,使项目能够及时从本地代码变为可复现的仓库版本。
严翊郗认为,李蔓淇在梳理测试场景和博客结构时比较细致,能够把功能要求转化为可验证的测试项。两人也认识到,结对开发中应更早确定分工和每次提交的粒度,例如先提交可运行的基础版本,再分别提交测试、性能分析和文档,这样提交历史会更清晰,也能减少最后集中整理的压力。
通过本次项目,我们不仅复习了分数运算、递归和表达式优先级,更实践了需求拆分、自动化测试、性能测量、版本控制与结对沟通。后续会继续保持“小步提交、先测试再重构、问题及时同步”的协作习惯。

浙公网安备 33010602011771号