首页
/ freeCodeCamp Daily Coding Challenge 284:用正则实现 "I Before E Except After C" 拼写校正

freeCodeCamp Daily Coding Challenge 284:用正则实现 "I Before E Except After C" 拼写校正

2026-09-09 11:47:50作者:沈韬淼Beryl

导读

本文围绕 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 对应块;
  • 每个块的元数据记录了 isUpcomingChangeusesMultifileEditorhelpCategory: "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" 拼写习惯的词改回正确拼写,具体规则只有三条:

  1. 如果单词中包含 "ei",且其前面不是 "c",则把它替换成 "ie"(例如 beleivebelieve);
  2. 如果单词中包含 "ie",且其前面 "c",则把它替换成 "ei"(例如 recievereceive);
  3. 其余所有单词保持不变。

需要注意的是:规则 1 的"前面不是 c"与规则 2 的"前面是 c"互为补集,因此任何出现 eiie 连续字符的单词都会命中其中一条规则;不包含这两个连续字符的单词则原样返回。

三、测试用例:五组输入输出

题目通过 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" 一句中同时出现 eiieieei 两类修正
"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" 单词内部可多次命中、deceitcei 反向验证、relief/thief/grief 等正向验证

最后一组用例特别值得注意:deceit 本就是 cei(c 后跟 ei),它是"正确"的拼写,不应该被改坏;而 recievedtheifbreiffeirce 都属于错误拼写,需要逐一纠正。它同时验证了函数不会对正确的 cei 造成破坏性修改。

测试断言的写法因语言而异:

  • JavaScript 版直接使用 assert.equal(iBeforeE(...), "..."),参见 JS 挑战文件
  • Python 版通过 runPython 内嵌 unittestTestCase().assertEqual(...),参见 Python 挑战文件

四、起始代码与解题思路

题目给出如下的函数骨架(seed),学习者需要填充实现:

function iBeforeE(sentence) {

  return sentence;
}

Python 版对应骨架:

def i_before_e(sentence):

    return sentence

核心难点在于如何精确表达"ei 前面不是 c"。直接查找子串 "ei" 会误伤 recieve 这类 cie 结构——因为其中也包含 eirec-i-ei-ve...cei...ei 前面正是 c)。因此必须借助"前向上下文"来判断。

解题思路可以分三步:

  1. split(' ') 把句子按空格拆成单词数组(题目用例中单词间均为单个空格);
  2. 对每个单词应用两条替换规则;
  3. 重新用空格拼接,保持原句结构不变。

五、官方解法: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" 为例走一遍:

  1. 拆分为 ["we", "recieved", "a", "breif"]
  2. we:无 ei/ie,不变;
  3. recieved:第一次替换,[^c]ei 需要非 c 前缀,recieved 中的 ei 位于 c 之后(re-ci-ei-ved),不匹配;第二次替换 ciecei,得到 received
  4. a:不变;
  5. breif:第一次替换,([^c])ei[^c] 捕获 r$1ie 得到 brief;第二次替换无命中;
  6. 拼接回 "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 的正确词不可被改坏:如 deceitceiling。它们命中第二条规则 ciecei?不会——它们本身是 cei,不含 cie;第一条规则要求 [^c]ei,而它们的 ei 前缀是 c,因此两次替换都不命中,保持不变,符合规则 3;
  • 多词句子与标点:本题用例只出现空格分隔、无标点的单词。若单词带句号/逗号(如 "breif,"),([^c])ei[^c] 也能匹配非 c 字符,仍可正确修正;但把 ei 出现在词首(如专有名词 "Eiffel")的情况不在本用例覆盖内;
  • 大小写[^c] 只匹配小写 c/cie/ 同样区分大小写,因此大写 Cie 结构不会被处理。本题用例均为小写;
  • 重叠命中g 标志与 re.sub 默认替换全部非重叠命中。像 theifei 前后分别是 hf,一次性替换即可;像 feirceei(前为 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(每日日历)、widgetnot-found 等组件(含 calendar.test.tsx),呈现每日一题入口;
  • 数据播种tools/daily-challenges/ 下的 seed-daily-challenges.tsREADME 说明了完整流程:复制 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+ / Python re(Python 3.6+ 可变长度后行)支持的可选写法;但注意 cie 情形仍需第二次替换兜底;
  • 处理标点与大小写:尝试先提取纯字母 token 再修正,最后还原标点与大小写,使函数能处理带句号、逗号、首字母大写的真实文本。

结语

Challenge 284 "I Before E" 是一道教科书级的正则字符串处理题:规则简洁,却要求对捕获组、全局替换与替换顺序有准确理解。它在 freeCodeCamp 仓库中的完整呈现(JS/Python 双语言、5 组断言、schema 校验、成对测试、播种脚本)也为学习者展示了一个真实开源项目中"一道编程题从文档到线上"的标准工程化路径。建议结合上述源码路径逐一阅读,把题目本身与它所在的工程链路一起消化。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23