第三次作业:结对项目

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

作业 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. 优化思路

性能主要受随机表达式构造、分数约分和重复检测影响。对应的改进如下:

  1. 使用 BigInt 和最大公约数进行精确约分,不使用浮点数,也不需要将分数反复转换为字符串参与运算。
  2. 每个表达式生成规范键后立即放入 Set,平均 O(1) 完成重复检测;不需要两两比较所有题目,避免 O(n²) 的重复判断。
  3. 将运算符数量限制为 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 提交和问题定位方面推进较快,使项目能够及时从本地代码变为可复现的仓库版本。

严翊郗认为,李蔓淇在梳理测试场景和博客结构时比较细致,能够把功能要求转化为可验证的测试项。两人也认识到,结对开发中应更早确定分工和每次提交的粒度,例如先提交可运行的基础版本,再分别提交测试、性能分析和文档,这样提交历史会更清晰,也能减少最后集中整理的压力。

通过本次项目,我们不仅复习了分数运算、递归和表达式优先级,更实践了需求拆分、自动化测试、性能测量、版本控制与结对沟通。后续会继续保持“小步提交、先测试再重构、问题及时同步”的协作习惯。

posted @ 2026-09-22 20:10  Yanyixi  阅读(8)  评论(0)    收藏  举报