freeCodeCamp 认证项目实战:用 Python 实现 Arithmetic Formatter 算术竖式格式化器
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
导读:本文完整解析 freeCodeCamp “Scientific Computing with Python(科学计算与 Python)”认证的第一个实战项目——Arithmetic Formatter。它要求实现一个
arithmetic_arranger函数,把一组加法/减法算式按小学生竖式计算的格式纵向排版并横向拼接,同时正确处理五类非法输入并返回固定的错误字符串。读完本文,你将掌握该挑战的完整需求规格、边界条件、官方参考解法的逐行原理,以及仓库中这一挑战的自动化测试如何在浏览器内运行的机制,可据此直接动手实现并交付项目。
一、项目背景:这是什么样的挑战?
Arithmetic Formatter 是 freeCodeCamp 课程体系中“Scientific Computing with Python”认证下、由学员独立从零构建的五个认证项目中的第一个。在认证定义文件 scientific-computing-with-python.yml 中可以看到完整清单,顺序如下:
| 顺序 | 项目 | 挑战 ID |
|---|---|---|
| 1 | Arithmetic Formatter | 5e44412c903586ffb414c94c |
| 2 | Time Calculator | 5e444136903586ffb414c94d |
| 3 | Budget App | 5e44413e903586ffb414c94e |
| 4 | Polygon Area Calculator | 5e444147903586ffb414c94f |
| 5 | Probability Calculator | 5e44414f903586ffb414c950 |
认证目录中把该项目称作 “Arithmetic Formatter”,对应的课程结构定义(superblock)位于 scientific-computing-with-python.json,其中 "Learn the bisection method by finding the square root of a number" 之后即是本项目的 block。而这个 block 的单独描述文件 build-an-arithmetic-formatter-project.json 提供了三项重要元信息:
"challengeOrder":整个 block 只包含这一道挑战,即“一个 block 一个项目”;"usesMultifileEditor": true:学员在多文件编辑器中编写解决方案;"helpCategory": "Python":帮助分类是 Python。
挑战正文本身存放在 5e44412c903586ffb414c94c.md。它的挑战类型编号为 23;在 packages/shared/src/config/challenge-types.ts 中可以看到 freeCodeCamp 对多种挑战类型的枚举(如multifileCertProject、lab等),本挑战属于项目类题目:不限制实现方式,只看函数行为和运行结果是否通过测试。
二、任务目标与输入输出契约
小学生常常把算术题排成竖式以便计算。例如"235 + 52"会被排成:
235 + 52 -----你的任务是补全arithmetic_arranger函数:它接收一个由算术题字符串组成的列表(每个字符串形如"32 + 698"),并把每道题纵向排版后横向并排返回。函数还有一个可选的第二个参数,当它取值为True时,输出中需要额外展示计算结果。
这是挑战给出的种子代码(seed),位于正文的--seed-contents--部分:
def arithmetic_arranger(problems, show_answers=False): return problems print(f'\n{arithmetic_arranger(["32 + 698", "3801 - 2", "45 + 43", "123 + 49"])}')注意两点约定:
- 第二个参数在规格里写作
show_answers(参考解法中命名为result),默认值为False,即默认不显示答案; - 函数在输入合法时返回排版好的多行字符串;在输入非法时返回描述错误含义的字符串(不是抛异常)。
输出示例一(不显示答案)
函数调用:
arithmetic_arranger(["32 + 698", "3801 - 2", "45 + 43", "123 + 49"])输出:
32 3801 45 123 + 698 - 2 + 43 + 49 ----- ------ ---- -----输出示例二(显示答案)
函数调用:
arithmetic_arranger(["32 + 8", "1 - 3801", "9999 + 9999", "523 - 49"], True)输出:
32 1 9999 523 + 8 - 3801 + 9999 - 49 ---- ------ ------ ----- 40 -3800 19998 474对照这两个示例即可验证排版细节:第一行是被减数/加数(第一操作数),第二行是运算符与第二操作数,第三行是由破折号构成的分隔线,第四行(仅当第二参数为True时)是逐题计算结果,结果可为负数。
三、格式规则逐条拆解
当输入格式正确时,返回值必须遵守以下格式规则:
- 运算符与较长操作数之间留一个空格,运算符与被减数/加数(第二操作数)位于同一行;
- 两个操作数保持用户给出的顺序:第一个操作数在上(顶行),第二个操作数在下(底行);
- 所有数字右对齐;
- 每道题之间用四个空格分隔;
- 每道题底部有一行破折号,破折号覆盖该道题自身的完整宽度(见上文示例)。
这些规则决定了“每道题的列宽”这一核心度量。若记该题两个操作数中较长的长度为max_len,则该题的占位宽度为max_len + 2,多出的两个字符恰好容纳“运算符 + 一个空格”。
四、错误处理:五类非法输入与精确的错误字符串
函数在遇到非法输入时必须返回下面这些固定的错误字符串,逐字符不能有差异(自动化测试按精确字符串比对,且常见于编辑器字符计数被公开源码引用,因此不能随意增删空格或标点):
| 错误场景 | 返回的错误字符串 |
|---|---|
| 一次提供的算术题超过5道 | 'Error: Too many problems.' |
运算符不是+或-(如*、/) | "Error: Operator must be '+' or '-'." |
| 某个操作数中含有数字以外的字符 | 'Error: Numbers must only contain digits.' |
| 某个操作数超过4 位宽度 | 'Error: Numbers cannot be more than four digits.' |
需要强调的两个判定要点:
- “只含数字”先于“位数”判定并不被测试依赖。测试中每一个错误用例都只触发一种错误(其余部分合法),因此实现时把位数检查放在纯数字检查之前(如官方参考解法那样)也能通过全部测试。但从业务逻辑上,建议先校验“纯数字”,再校验“位数”,语义更严谨。
- 规格同时声明:“其它未在此处列出的运算符不需要被测试。”即你只需要处理
+、-、*、/之外不会被判定的情况,无需过度设计。
规格还附了一条调试提示:按下 F12 打开浏览器控制台,可以看到更详细的测试输出信息(verbose output),方便逐条核对失败原因。
五、自动化测试与验收标准
挑战正文的--hints--部分内置了 10 条断言,全部用 Pythonunittest的TestCase().assertEqual(...)写成,通过 freeCodeCamp 前端测试框架的runPython(...)注入执行。在客户端源码 python-worker-handler.ts 中可以看到机制:解决方案代码会被发送给独立的python-worker.js(一个浏览器内的 Python Web Worker)执行,测试再与返回结果逐字比对。下表把这 10 条用例整理为可直接照做的验收清单:
| # | 函数调用 | 期望返回值 |
|---|---|---|
| 1 | arithmetic_arranger(["3801 - 2", "123 + 49"]) | ' 3801 123\n- 2 + 49\n------ -----' |
| 2 | arithmetic_arranger(["1 + 2", "1 - 9380"]) | ' 1 1\n+ 2 - 9380\n--- ------' |
| 3 | arithmetic_arranger(["3 + 855", "3801 - 2", "45 + 43", "123 + 49"]) | ' 3 3801 45 123\n+ 855 - 2 + 43 + 49\n----- ------ ---- -----' |
| 4 | arithmetic_arranger(["11 + 4", "3801 - 2999", "1 + 2", "123 + 49", "1 - 9380"]) | ' 11 3801 1 123 1\n+ 4 - 2999 + 2 + 49 - 9380\n---- ------ --- ----- ------' |
| 5 | arithmetic_arranger(["44 + 815", "909 - 2", "45 + 43", "123 + 49", "888 + 40", "653 + 87"]) | 'Error: Too many problems.' |
| 6 | arithmetic_arranger(["3 / 855", "3801 - 2", "45 + 43", "123 + 49"]) | "Error: Operator must be '+' or '-'." |
| 7 | arithmetic_arranger(["24 + 85215", "3801 - 2", "45 + 43", "123 + 49"]) | 'Error: Numbers cannot be more than four digits.' |
| 8 | arithmetic_arranger(["98 + 3g5", "3801 - 2", "45 + 43", "123 + 49"]) | 'Error: Numbers must only contain digits.' |
| 9 | arithmetic_arranger(["3 + 855", "988 + 40"], True) | ' 3 988\n+ 855 + 40\n----- -----\n 858 1028' |
| 10 | arithmetic_arranger(["32 - 698", "1 - 3801", "45 + 43", "123 + 49", "988 + 40"], True) | ' 32 1 45 123 988\n- 698 - 3801 + 43 + 49 + 40\n----- ------ ---- ----- -----\n -666 -3800 88 172 1028' |
判定细节提醒:
- 用例 4 是 5 道题的上限验证(合法输入),用例 5 的 6 道题则触发 “Too many problems.”;
- 用例 10 同时覆盖负数结果的对齐:
-666、-3800在所在列内依然是右对齐的; - 所有多行字符串在比较时用
\n连接,行尾不允许有残留空格(这要求对每行做去尾随空格处理)。
六、实现思路:两种解法深入讲解
方案一:官方参考解法(--solutions--)
官方给出的参考解法非常“教科书式”,用四个字符串累加器分别积累四行内容,最后拼合:
def arithmetic_arranger(problems, result=False): lin1 = "" lin2 = "" lin3 = "" lin4 = "" if len(problems) > 5: return 'Error: Too many problems.' for problem in problems: [num1, sym, num2] = problem.split() sign = ['+', '-'] if sym not in sign: return ("Error: Operator must be '+' or '-'.") if len(num1) > 4 or len(num2) > 4: return ("Error: Numbers cannot be more than four digits.") if not num1.isnumeric() or not num2.isnumeric(): return ("Error: Numbers must only contain digits.") lin1 += " " + num1 + " " if len(num1) >= len( num2) else " " * (len(num2) + 2 - len(num1)) + num1 + " " lin2 += sym + " " + num2 + " " if len(num2) >= len( num1) else sym + " " * (len(num1) - len(num2) + 1) + num2 + " " nmax = (len(num1) + 2) if len(num1) >= len(num2) else (len(num2) + 2) lin3 += "-" * nmax + " " op = int(num1) + int(num2) if sym == "+" else int(num1) - int(num2) lin4 += (" " * (nmax - len(str(op)))) + str(op) + " " arranged_problems = lin1.rstrip() + "\n" + lin2.rstrip( ) + "\n" + lin3.rstrip() if result: arranged_problems += "\n" + lin4.rstrip() return arranged_problems逐段理解它:
- 前置校验:先判题数上限;进入循环后对每题依次校验运算符、位数上限、纯数字。注意它对
problem.split()的使用:默认按空白切分恰好得到[操作数1, 运算符, 操作数2]三个元素,天然约束了输入必须形如"a + b"。 - 第 1 行(lin1):当
num1不少于num2时,前缀两个空格恰好是" " + num1,使顶行与第 2 行宽度一致(运算符位 + 空格位被顶行用两个空格占位);否则用" " * (len(num2) + 2 - len(num1))补齐右对齐。尾部统一追加 4 个空格作为题间距。 - 第 2 行(lin2):运算符紧贴
num2,二者之间恰有一个空格(规则 1);若num1更长,则在运算符后用" " * (len(num1) - len(num2) + 1)补足让num2右对齐。细看会发现它把“运算符 + 一个空格”固定为第 2 行的最左侧内容。 - 第 3 行(lin3):破折号数量为
nmax = 最长操作数长度 + 2(覆盖运算符与空格位),与格式规则 5 对应。 - 第 4 行(lin4):先按
+/-计算整数结果,再用空格把结果右对齐到nmax宽度。负数结果的-号自然被len(str(op))计入宽度。 - 收尾:每一行都用
.rstrip()去掉行尾的 4 个空格(题与题之间的间距不能残留在整行末尾),再以"\n"拼接前三行;仅当result为真时追加答案行。这正是“行尾无空格”验收点的来源。
方案二:基于列表 +str.join的可读写法
官方解法用字符串原地追加,可读性一般。更推荐先用列表积累每行,最后统一 join。下面是一个等价实现(注意缩进使用空格,与 PEP 8 一致):
def arithmetic_arranger(problems, show_answers=False): if len(problems) > 5: return 'Error: Too many problems.' first_line, second_line, dashes_line, answers_line = [], [], [], [] for problem in problems: num1, operator, num2 = problem.split() if operator not in '+-': return "Error: Operator must be '+' or '-'." if not (num1.isdigit() and num2.isdigit()): return 'Error: Numbers must only contain digits.' if len(num1) > 4 or len(num2) > 4: return 'Error: Numbers cannot be more than four digits.' width = max(len(num1), len(num2)) + 2 first_line.append(num1.rjust(width)) second_line.append(operator + num2.rjust(width - 1)) dashes_line.append('-' * width) if show_answers: answer = str(int(num1) + int(num2)) if operator == '+' \ else str(int(num1) - int(num2)) answers_line.append(answer.rjust(width)) arranged = ' '.join(first_line) + '\n' + \ ' '.join(second_line) + '\n' + ' '.join(dashes_line) if show_answers: arranged += '\n' + ' '.join(answers_line) return arranged与官方解法的关键差异在于:
- 用
num.rjust(width)直接完成右对齐,避免手写空格乘法,错误率更低; - 第 2 行写作
operator + num2.rjust(width - 1):运算符占 1 个字符、其后width - 1个位置留给空格 + 右对齐的操作数,恰好保证“运算符与较长的操作数之间有一个空格”(若num2就是较长者,则补足后仍正好 1 个空格); - 用
' '.join(...)(四个空格)拼接每题,天然形成题间距; - 校验顺序调整为“运算符 → 纯数字 → 位数”,语义更清晰,同样能通过全部 10 条测试。
七、本地运行与自测指南
挑战在站点内使用多文件编辑器提交,但完全可以在本地先验证。由于函数只依赖 Python 标准库(str、int),在任意 Python 3 环境中即可运行,步骤如下:
- 将你的实现保存为
arithmetic_formatter.py; - 在文件末尾追加来自种子代码的调用与输出示例;
- 用第五节的 10 条验收用例逐条核对,重点检查:
- 直接对多行字符串做
==比较(而不仅是肉眼查看),确保行尾无空格; - 负号结果(如
-3800、-666)在列内右对齐; - 5 道题合法、6 道题报
'Error: Too many problems.'的边界。
- 直接对多行字符串做
一个可复制的快速自测脚本片段:
import unittest class TestArithmeticFormatter(unittest.TestCase): def test_two_problems(self): expected = ' 3801 123\n- 2 + 49\n------ -----' self.assertEqual( arithmetic_arranger(["3801 - 2", "123 + 49"]), expected) def test_answers_shown(self): expected = (' 3 988\n+ 855 + 40\n' '----- -----\n 858 1028') self.assertEqual( arithmetic_arranger(["3 + 855", "988 + 40"], True), expected) def test_too_many_problems(self): self.assertEqual( arithmetic_arranger(["1 + 2"] * 6), 'Error: Too many problems.') if __name__ == '__main__': unittest.main()八、易错点与经验小结
- 返回值必须是无额外前后缀的字符串。提交到站点时,输出会先经
print再交给测试比对,若在函数内擅自print排版结果而非return,会导致返回值恒为None,测试全部失败。 - 校验用固定字符串。错误信息中的引号与标点必须与原规格完全一致——例如 “Operator must be '+' or '-'.” 里单引号包围
+和-,多行输出由字面\n换行符分隔。 - “每题各自一行破折号”与“列宽自适应”是一体的。破折号数量 = 该题两个操作数中较长者的位数 + 2,若强行按最大题宽统一画线会导致较窄题目底部多出横线而判定失败。
- 行尾空格是隐藏杀手。肉眼难以察觉,推荐自测断言用
==精确比对,或对每行.rstrip()后再拼合。 - 操作数的顺序不可交换。顶行必须是第一个操作数、底行是第二个,减法中二者顺序决定结果正负,与示例 10 中的负数输出直接相关。
该项目作为 Scientific Computing with Python 认证的开篇之作,训练的核心恰好是字符串格式化、边界校验和“精确匹配外部契约”的工程习惯——这些能力会在此后 Time Calculator、Budget App 等项目中被反复用到。
九、在仓库中继续深挖的入口
如果你希望了解该挑战在课程体系与测试链路中的更多实现细节,可以从以下仓库路径继续追踪:
- 挑战规格正文:5e44412c903586ffb414c94c.md(含描述、hints、seed 与官方 solution);
- 所在 block 的结构配置:build-an-arithmetic-formatter-project.json;
- 科学计算认证的 superblock 课程顺序:scientific-computing-with-python.json;
- 该认证 5 个项目的入口清单:scientific-computing-with-python.yml;
- 前端如何在浏览器内执行你的 Python 代码并对结果跑测试:python-worker-handler.ts;
- freeCodeCamp 挑战类型(本项目对应编号 23)的枚举定义:challenge-types.ts。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考