freeCodeCamp 每日编码挑战解析:Challenge 330「lowercase words」过滤全小写单词的实现与验证
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇技术指南围绕 freeCodeCamp 开源课程仓库中的每日编码挑战Challenge 330: lowercase words(挑战源文件)展开,完整解读题目要求、测试用例与官方参考解法,并结合仓库中每日编码挑战的课程结构、前后端实现与数据灌入脚本,说明这类挑战在 freeCodeCamp 中的实际运行链路。读完本文,你将掌握基于split/filter/join流水线处理字符串过滤问题的方法,并理解每日编码挑战从 Markdown 挑战文件到数据库、再到前端渲染与断言验证的完整机制。
一、挑战背景:什么是 freeCodeCamp 每日编码挑战
在进入具体题目之前,先交代它所在的环境。freeCodeCamp 仓库中维护着一个独立的课程块(block),名为daily-coding-challenges-javascript,其元数据定义在 curriculum/structure/blocks/daily-coding-challenges-javascript.json。该块共包含365 道挑战(Challenge 1 至 Challenge 365),按天发布,isUpcomingChange为true,采用legacy-challenge-list布局,并启用usesMultifileEditor(多文件编辑器)。Challenge 330 正是其中第 330 道,紧邻 Challenge 329 "Bucket Fill" 与 Challenge 331 "Nearest Multiple"。
从块结构可以推断,每日编码挑战以"一日一题"的形式存在:仓库根目录中的 tools/daily-challenges/seed-daily-challenges.ts 将挑战按每日递增的日期批量写入数据库,第一条挑战对应2025-08-11(UTC),此后每条挑战日期顺延一天;该脚本还以常量EXPECTED_CHALLENGE_COUNT = 365校验挑战数量,确保一年 365 天每天都有题可练。
二、题目要求逐字解读
Challenge 330 的英文原题如下(位于挑战文件的# --description--部分):
Given a string, return only the words that are entirely lowercase, in their original order and with a space between each word.
将其拆解为三条核心约束:
- 过滤条件:只保留"完全小写"的单词(entirely lowercase),即单词中不允许出现任何大写字母;
- 顺序保持:保留单词在原始字符串中的相对顺序(original order);
- 输出格式:每个保留的单词之间以一个空格分隔(with a space between each word)。
题目没有要求修改单词本身、没有要求去重、也没有要求排序,因此这是一个典型的"筛选(filter)"型字符串问题。值得注意的是,toLowerCase()只作用于 ASCII 字母大小写转换,因此像数字、标点这类"没有大小写之分"的字符不受影响——例如单词"123"与自身的小写形式相等,会被视为"全小写单词"而保留(这与参考解法中word === word.toLowerCase()的判断逻辑一致)。
三、测试用例(hints)与断言行为分析
挑战文件通过 5 组测试用例(# --hints--部分)定义了通过标准。freeCodeCamp 的挑战机制中,每个 hint 都对应一条由assert构成的测试字符串,只有全部通过才算挑战完成。下表完整列出这 5 组用例及其行为特征:
| 输入字符串 | 期望输出 | 覆盖的边界特征 |
|---|---|---|
"hello GOOD world" | "hello world" | 过滤中间的全大写单词GOOD |
"these are all lowercase" | "these are all lowercase" | 全小写输入应原样返回(不变式) |
"less is NoT more" | "less is more" | 过滤混合大小写单词NoT(首字母大写 + 全大写组合) |
"DonT eat pizza every OTHER day" | "eat pizza every day" | 同时过滤DonT与OTHER,保留单词间仍以单个空格连接 |
"the Super quick AND snEaky brown fox Leapt anD jumped over aNd AROUND the lazy SloW dog" | "the quick brown fox jumped over the lazy dog" | 长句中过滤多种大写形态(首字母大写、全大写、大小写混杂) |
这些用例的assert.equal断言写法如下(节选自 挑战源文件):
assert.equal(getLowercaseWords("hello GOOD world"), "hello world"); assert.equal(getLowercaseWords("DonT eat pizza every OTHER day"), "eat pizza every day");从测试设计上可以归纳出判定"全小写单词"的标准:只要单词中存在任何大写字母(无论出现在首字母还是单词中部),该单词就被排除。NoT、DonT、snEaky这类"单词内含大写"的用例专门用来防止解题者只按word[0]判断首字母大小写。
四、种子代码(seed)与官方参考解法
4.1 种子代码
挑战文件# --seed--部分提供了函数骨架,学习者需要在此完成实现:
function getLowercaseWords(str) { return str; }种子代码直接返回原字符串,意味着不实现任何逻辑时全部测试都会失败。每日编码挑战在课程中被归类为challengeType: 28(对应@freecodecamp/shared中定义的 JavaScript 每日挑战类型),在客户端渲染时通过多文件编辑器编辑,函数在浏览器环境中运行并被测试断言验证。
4.2 官方参考解法
挑战文件# --solutions--部分给出的官方解法仅用一行完成全部需求:
function getLowercaseWords(str) { return str.split(" ").filter(word => word === word.toLowerCase()).join(" "); }其执行流程可以拆解为三个步骤:
str.split(" "):以单个空格为分隔符,将字符串拆分为单词数组。注意这里使用的是普通空格" ",因此多个连续空格会产生空字符串元素(空字符串"" === "".toLowerCase()成立,会被filter保留,这是该解法对"多余空格"的一种隐性处理方式,但从测试用例看题目输入均为单词间单个空格);.filter(word => word === word.toLowerCase()):保留"与其全小写形式完全相等"的单词。这是整个挑战的核心判定:只要单词含有一个大写字母,word.toLowerCase()就会改变其内容,比较结果为false,单词被过滤;.join(" "):以单个空格连接过滤后的单词,保证输出中单词间只有一个空格,与题目"with a space between each word"的要求完全吻合。
这一解法充分利用了 JavaScript 字符串与数组的链式 API,是函数式风格(map/filter/reduce 思维)在字符串处理中的典型示范。
五、深入学习路径:可替换实现与易错点
为了让解题者不只停留在"背答案"层面,这里补充几种等价实现思路及其对应的易错点,可用于自行验证或拓展练习:
正则表达式方案:使用/\b[a-z]+\b/之类的模式匹配全部由小写字母组成的词,但需要注意\b词边界与标点、数字的交互,且正则方案无法直接复用上面 5 组用例的语义(例如单词含数字时行为会与参考解法不同),因此以通过挑战测试为准,推荐使用与官方解法语义完全一致的word === word.toLowerCase()判定。
显式循环方案:
function getLowercaseWords(str) { const words = str.split(" "); const result = []; for (const word of words) { if (word === word.toLowerCase()) { result.push(word); } } return result.join(" "); }容易踩坑的点:
- 只判断首字母大小写(如
word[0] === word[0].toLowerCase()),会错误地保留NoT、DonT这类含内部大写字母的单词; - 使用
word === word.toUpperCase()反推"全大写"再取反,会误伤"123"、"!!"等不含字母的 token(它们toUpperCase()后仍等于自身); - 忘记
join(" "),返回数组而不是字符串,导致assert.equal全部失败; - 使用
replace或match时未考虑大小写混合单词被整体剔除的语义。
六、从挑战文件到线上运行的完整链路
Challenge 330 并非孤立的练习题文件,它处于 freeCodeCamp 每日编码挑战的完整生产链路中。结合仓库源码,这条链路可以分为四段:
6.1 课程侧:挑战文件与结构定义
挑战的原始内容(题目描述、hints、seed、solution)存放在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/ 下,每个挑战一个 Markdown 文件。其 frontmatter 中challengeType: 28是关键标记:它让 curriculum/schema/challenge-schema.js 这类 Joi 校验(challengeType: Joi.number().min(0).max(33).required())以及课程构建流程能识别出这是一道每日编码挑战,并按对应模板渲染。
6.2 数据侧:GraphQL 抽取与数据库灌入
tools/daily-challenges/seed-daily-challenges.ts 配合 tools/daily-challenges/helpers.ts 完成挑战数据入库:
- 脚本通过 GraphQL 查询(端点
http://localhost:8000/___graphql,需要客户端以"显示即将上线内容"模式运行)从dev-playground超级块中按challengeOrder升序拉取全部 JavaScript 与 Python 挑战; combineChallenges将同一编号的 JS/Python 挑战合并为一条文档,写入 MongoDB 的DailyCodingChallenges集合,并使用挑战的id作为文档_id(代码注释强调"DO NOT CHANGE THE ID",因为它同时被completedDailyCodingChallenges[]引用);- 脚本内置多重一致性校验:JS 与 Python 挑战数量必须相等、标题必须一致、描述必须一致、测试数量必须一致,数量必须等于 365,任何一项不满足都会抛出错误中断灌入;
- 每条挑战按
START_DATE(2025-08-11 UTC)加上i * ONE_DAY_IN_MS生成日期,脚本对开始日期做了硬编码保护,防止发布后意外改动。
灌入后的文档结构为:_id、challengeNumber、title(已去掉Challenge N:前缀)、date、description(已去除解析器添加的<section id="description">包装,见removeSection)、javascript.tests/challengeFiles与python.tests/challengeFiles。
6.3 API 侧:按日期提供挑战内容
api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 注册了 6 个公开的 GET 路由,供客户端按日期或天数获取挑战:
GET /daily-coding-challenge/date/:date:按YYYY-MM-DD精确日期查询,日期晚于今天(美国中部时间)会返回 404;GET /daily-coding-challenge/day/:day:按MM-DD查询,内部通过getSourceDate换算到源日期(Challenge 330 对应的2026-06-19这类日期即由该逻辑得出);GET /daily-coding-challenge/today:返回"今天"的挑战;GET /daily-coding-challenge/month/:month:按YYYY-MM返回当月挑战列表(只含id、challengeNumber、date、title),且只返回不晚于今天的记录;GET /daily-coding-challenge/all:返回全部挑战的简要列表;GET /daily-coding-challenge/newest:返回最新一条挑战的日期。
路由全部使用 Fastify + TypeBox schema 做参数校验(定义于 api/src/daily-coding-challenge/schemas/),非法日期格式返回 400,未找到返回 404,并对各类结果打点 Sentry 指标(如dcc.challenge_viewed、dcc.challenge_not_found)。挑战的提交不经过这些路由,而是走主挑战完成流程(见 api/src/daily-coding-challenge/README.md)。
6.4 客户端侧:拉取、校验与渲染
client/src/client-only-routes/show-daily-coding-challenge.tsx 是前端渲染入口:
- 根据 URL 中的日期调用
/daily-coding-challenge/day/{MM-DD}获取挑战数据; - 使用 client/src/utils/daily-coding-challenge-validator.ts 对返回数据做 schema 校验,校验失败则抛出错误并显示"未找到"页面;
formatChallengeData将数据库文档重新组装为ShowClassic组件所需的 props,其中 JavaScript 侧challengeType: 28、helpCategory: 'JavaScript',文件内容来自javascript.challengeFiles[0].contents,测试来自javascript.tests——Challenge 330 的 5 组assert.equal正是以这种方式进入浏览器测试运行器的;- 用户在编辑器内编辑
getLowercaseWords,点击运行后由测试断言逐条验证,全部通过即完成挑战。
此外,client/src/components/daily-coding-challenge/widget.tsx 提供了首页/学习地图上的入口组件,将用户引导至/learn/daily-coding-challenge/{MM-DD}当天的题目页面。
七、在本地运行与调试 Challenge 330
若要在本地环境亲手验证 Challenge 330 的解法,可以按以下方式操作(仓库为只读,以下均只涉及查看与本地运行):
- 本地校验解法正确性:在 Node.js 环境直接粘贴参考解法与 5 组断言即可快速验证:
function getLowercaseWords(str) { return str.split(" ").filter(word => word === word.toLowerCase()).join(" "); } console.log(getLowercaseWords("hello GOOD world")); // "hello world" console.log(getLowercaseWords("DonT eat pizza every OTHER day")); // "eat pizza every day" console.log(getLowercaseWords( "the Super quick AND snEaky brown fox Leapt anD jumped over aNd AROUND the lazy SloW dog" )); // "the quick brown fox jumped over the lazy dog"查看课程结构与相邻挑战:块定义 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中
challengeOrder完整列出了 365 道挑战的 id 与标题,Challenge 330 的 id 为6a1d9f98e819ed70a0e994dc,可通过它定位同目录下的相邻挑战(329、331)作对比练习。本地完整链路运行(可选):按照 tools/daily-challenges/README.md 的指引,将
sample.env复制为.env、安装依赖、以"显示即将上线内容"模式运行客户端,然后执行pnpm seed-daily-challenges将挑战灌入本地 MongoDB,即可配合 API 与前端页面复现完整的每日挑战体验。注意该流程面向本地开发环境,脚本本身也提醒生产库的开始日期不应改动。
八、小结
Challenge 330 "lowercase words" 是一道精炼的字符串过滤题:其核心判定word === word.toLowerCase()一行代码便概括了"完全小写"的定义,配合split/filter/join组成标准的函数式字符串处理流水线。同时,从这道题出发可以看到 freeCodeCamp 每日编码挑战的工程全貌——挑战以 Markdown 文件为唯一事实来源,经 GraphQL 抽取、一致性校验与日期灌入写入数据库,再通过 Fastify API 按日/月/全部等维度对外提供,最终由客户端校验 schema、组装 props 并交给浏览器测试运行器逐条断言。理解这条链路,既能帮助你快速通过本挑战,也能为你参与 freeCodeCamp 课程生态的阅读与维护提供清晰的上下文。
延伸阅读:挑战相邻题目(Challenge 329: Bucket Fill、Challenge 331: Nearest Multiple)可作同类型字符串/数学练习;每日挑战的 Python 版本位于同结构块的daily-coding-challenges-python中。
【免费下载链接】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),仅供参考