freeCodeCamp Daily Coding Challenge 284:用正则实现 "I Before E Except After C" 拼写校正
导读
本文围绕 freeCodeCamp 开源仓库中 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69f35a5bb823ed620fcb7cbd.md(Challenge 284: I Before E)展开,完整拆解这道字符串处理编程题的题目规则、5 组测试用例、JavaScript 与 Python 双语言官方参考解法,并结合仓库中 daily coding challenge 体系的配套代码(块结构定义、成对校验测试、种子脚本与前后端组件)讲清它在整个项目中的落地方式。读完本文,你将掌握一类"基于相邻字符上下文做字符串替换"问题的分析与实现方法,并理解 freeCodeCamp 的每日编程挑战从题库到测试再到数据播种的完整链路。
一、题目背景:什么是 freeCodeCamp Daily Coding Challenge
Daily Coding Challenge(每日编程挑战)是 freeCodeCamp 平台上一套按天更新、题型短小的编程练习。在本仓库中,它们以课程块(block)的形式存在于 "Dev Playground" 超级块下,包含 JavaScript 与 Python 两套平行实现:
- 块定义见 curriculum/structure/blocks/daily-coding-challenges-javascript.json(共 365 道,JavaScript 版)与其 Python 对应块;
- 每个块的元数据记录了
isUpcomingChange、usesMultifileEditor、helpCategory: "JavaScript"、disableLoopProtectTests以及challengeOrder中按id排序的全部挑战标题; - 从 curriculum/src/test/daily-challenges.test.js 的校验逻辑可以看到:系统要求 JS 与 Python 两个块的挑战数量完全相等、id 完全一致,且非英语环境以外还要求标题与描述逐一匹配——Challenge 284 正是同时存在于两个块中的一题(JS 版
challengeType: 28,Python 版challengeType: 29)。
Challenge 284 排在挑战序列的第 284 位,紧随 Challenge 283 "String Zipper",紧邻 Challenge 285 "Meeting Time",是 365 道挑战中后段的一道字符串处理题。
二、题目规则:I before E except after C
原题描述如下:
Given a word or sentence, return a corrected version where every word follows the "I before E except after C" rule.
它要求我们把一句英语中不符合 "I before E except after C" 拼写习惯的词改回正确拼写,具体规则只有三条:
- 如果单词中包含
"ei",且其前面不是"c",则把它替换成"ie"(例如beleive→believe); - 如果单词中包含
"ie",且其前面是"c",则把它替换成"ei"(例如recieve→receive); - 其余所有单词保持不变。
需要注意的是:规则 1 的"前面不是 c"与规则 2 的"前面是 c"互为补集,因此任何出现 ei 或 ie 连续字符的单词都会命中其中一条规则;不包含这两个连续字符的单词则原样返回。
三、测试用例:五组输入输出
题目通过 5 个断言(hints)定义验收标准,它们也是仓库中实际运行的测试:
| 输入 | 预期输出 | 覆盖点 |
|---|---|---|
"beleive" |
"believe" |
单次 ei(前非 c)→ ie |
"recieve" |
"receive" |
单次 ie(前为 c)→ ei |
"we recieved a breif" |
"we received a brief" |
多词句子、大小写保持、非命中词(a)不变 |
"she beleived the friendly niece could percieve the greif" |
"she believed the friendly niece could perceive the grief" |
一句中同时出现 ei→ie 与 ie→ei 两类修正 |
"we recieved relief after the theif gave us a breif piece of feirce deceit" |
"we received relief after the thief gave us a brief piece of fierce deceit" |
单词内部可多次命中、deceit 中 cei 反向验证、relief/thief/grief 等正向验证 |
最后一组用例特别值得注意:deceit 本就是 cei(c 后跟 ei),它是"正确"的拼写,不应该被改坏;而 recieved、theif、breif、feirce 都属于错误拼写,需要逐一纠正。它同时验证了函数不会对正确的 cei 造成破坏性修改。
测试断言的写法因语言而异:
- JavaScript 版直接使用
assert.equal(iBeforeE(...), "..."),参见 JS 挑战文件; - Python 版通过
runPython内嵌unittest的TestCase().assertEqual(...),参见 Python 挑战文件。
四、起始代码与解题思路
题目给出如下的函数骨架(seed),学习者需要填充实现:
function iBeforeE(sentence) {
return sentence;
}
Python 版对应骨架:
def i_before_e(sentence):
return sentence
核心难点在于如何精确表达"ei 前面不是 c"。直接查找子串 "ei" 会误伤 recieve 这类 cie 结构——因为其中也包含 ei(rec-i-ei-ve 中 ...cei... 的 ei 前面正是 c)。因此必须借助"前向上下文"来判断。
解题思路可以分三步:
- 用
split(' ')把句子按空格拆成单词数组(题目用例中单词间均为单个空格); - 对每个单词应用两条替换规则;
- 重新用空格拼接,保持原句结构不变。
五、官方解法:JavaScript 正则版本
仓库中 JS 版官方参考解法如下:
function iBeforeE(sentence) {
return sentence.split(' ').map(word => {
word = word.replace(/([^c])ei/g, '$1ie');
word = word.replace(/cie/g, 'cei');
return word;
}).join(' ');
}
逐行解读:
sentence.split(' '):按单个空格切分句子;word.replace(/([^c])ei/g, '$1ie'):正则([^c])ei匹配"一个非 c 字符紧跟着 ei"的组合,g标志确保单词内所有命中位置都被处理。([^c])是捕获组,$1ie表示把捕获到的那个字符原样保留、再把ei换成ie;word.replace(/cie/g, 'cei'):把cie(c 后接 ie)整体替换为cei;.join(' '):把修正后的单词重新拼回句子。
两次替换的顺序是安全的:第一次替换后,所有非 c 前缀的 ei 已变为 ie,剩余的 ei 只可能以 cei 形式存在,此时 cie 也就只可能来自原本的 ci 后接 ei(即 cie 原形),因此第二次替换不会误伤。
以 "we recieved a breif" 为例走一遍:
- 拆分为
["we", "recieved", "a", "breif"]; we:无ei/ie,不变;recieved:第一次替换,[^c]ei需要非 c 前缀,recieved中的ei位于c之后(re-ci-ei-ved),不匹配;第二次替换cie→cei,得到received;a:不变;breif:第一次替换,([^c])ei中[^c]捕获r,$1ie得到brief;第二次替换无命中;- 拼接回
"we received a brief",与断言一致。
六、官方解法:Python 正则版本
Python 版参考解法使用了 re 模块:
import re
def i_before_e(sentence):
words = []
for word in sentence.split(" "):
word = re.sub(r"([^c])ei", r"\1ie", word)
word = re.sub(r"cie", "cei", word)
words.append(word)
return " ".join(words)
与 JS 版一一对应:re.sub(r"([^c])ei", r"\1ie", word) 等价于 replace(/([^c])ei/g, '$1ie')(\1 即 JS 中的 $1),re.sub(r"cie", "cei", word) 等价于 replace(/cie/g, 'cei')。JS 用 map 返回新数组,Python 用显式 words.append 累积结果,逻辑完全同构。
七、边缘情况与隐藏陷阱分析
结合题目与解法,有几个值得注意的边界:
ci后跟ei的正确词不可被改坏:如deceit、ceiling。它们命中第二条规则cie→cei?不会——它们本身是cei,不含cie;第一条规则要求[^c]ei,而它们的ei前缀是c,因此两次替换都不命中,保持不变,符合规则 3;- 多词句子与标点:本题用例只出现空格分隔、无标点的单词。若单词带句号/逗号(如
"breif,"),([^c])ei的[^c]也能匹配非 c 字符,仍可正确修正;但把ei出现在词首(如专有名词"Eiffel")的情况不在本用例覆盖内; - 大小写:
[^c]只匹配小写c,/cie/同样区分大小写,因此大写Cie结构不会被处理。本题用例均为小写; - 重叠命中:
g标志与re.sub默认替换全部非重叠命中。像theif中ei前后分别是h和f,一次性替换即可;像feirce中ei(前为f)被第一条规则修正后,ce结构不再构成cie,不会产生二次误伤。
这些边界正是"基于上下文的正则替换"类题目的通用考察点:写正则时永远要思考零宽断言与捕获组的取舍、替换顺序对重叠模式的影响,以及替换是否会破坏已经正确的子串。
八、在仓库中的落地:从题库到线上
Challenge 284 并非孤立的一页文档,它嵌入在完整的 daily coding challenge 工程链路中:
- 题库与测试:挑战正文、断言与种子代码以 Markdown 前奏(frontmatter
id/challengeType/dashedName)存储在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/ 与同名的 Python 块目录下,由课程 schema 校验工具(curriculum/schema/challenge-schema.js)检查格式合法性; - 成对校验:curriculum/src/test/daily-challenges.test.js 通过
getChallenges加载 "Dev Playground" 超级块,断言 JS 与 Python 两个块挑战数相等、id 一一对应、测试数一致,保证双语言题库不漂移; - 后端服务:api/src/daily-coding-challenge/ 提供获取每日挑战信息的 Fastify 路由与 schema(routes、schemas、utils 三个子目录,含配套单测),其 README 说明"每日挑战的提交仍走 API 主体部分";
- 前端组件:client/src/components/daily-coding-challenge/ 提供
calendar(每日日历)、widget、not-found等组件(含calendar.test.tsx),呈现每日一题入口; - 数据播种:tools/daily-challenges/ 下的
seed-daily-challenges.ts与 README 说明了完整流程:复制sample.env为.env、启动开启"即将上线的更改"的客户端以从 GraphQL 拉取挑战、在tools/daily-challenges目录运行pnpm seed-daily-challenges,最终把 "Dev Playground" 超级块中的挑战写入freecodecamp数据库的DailyCodingChallenges集合。
也就是说,一道 Challenge 284 从 Markdown 题目出发,经由 schema 校验、双语言配对测试、GraphQL 提取与播种脚本,最终成为平台上可被每日调度的真实练习。
九、举一反三:同类问题的扩展练习
掌握了本解法的模式后,可以尝试以下变体巩固:
- 扩展到任意上下文规则:把"前一个字符"推广为"前两个字符",例如将
"ckei"特殊处理,只需把正则写成([^c])ei的变体(如(..)ei加回溯约束); - 用零宽断言重写:
word.replace(/(?<!c)ei/g, 'ie')使用后行断言(lookbehind)能更直观地表达"前面不是 c",是 JS 2022+ / Pythonre(Python 3.6+ 可变长度后行)支持的可选写法;但注意cie情形仍需第二次替换兜底; - 处理标点与大小写:尝试先提取纯字母 token 再修正,最后还原标点与大小写,使函数能处理带句号、逗号、首字母大写的真实文本。
结语
Challenge 284 "I Before E" 是一道教科书级的正则字符串处理题:规则简洁,却要求对捕获组、全局替换与替换顺序有准确理解。它在 freeCodeCamp 仓库中的完整呈现(JS/Python 双语言、5 组断言、schema 校验、成对测试、播种脚本)也为学习者展示了一个真实开源项目中"一道编程题从文档到线上"的标准工程化路径。建议结合上述源码路径逐一阅读,把题目本身与它所在的工程链路一起消化。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051