freeCodeCamp 每日编程挑战 15:toCamelCase 驼峰转换的题目拆解与源码级解析
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本文围绕 freeCodeCamp 课程库中 JavaScript 每日编程挑战(Daily Coding Challenges)的第 15 题——字符串驼峰命名转换(camelCase)展开:完整继承原题目的规格说明与全部官方测试用例,并基于课程块配置、挑战类型定义等仓库源码,讲解该题在平台中的组织方式、官方解法的每一步原理,以及实现时需要规避的典型陷阱。读完本文,你将能够独立实现一个能处理连续分隔符、大小写混杂输入的toCamelCase函数,并理解它在 freeCodeCamp 课程体系中的定位。
题目背景:这道题在课程体系中的位置
该题位于课程块daily-coding-challenges-javascript中,源文件为 Challenge 15: camelCase,其 Front Matter 中的关键元数据为:
id: 6821ebd4237de8297eaee791 title: "Challenge 15: camelCase" challengeType: 28 dashedName: challenge-15challengeType: 28对应每日编程挑战中的 JavaScript 类型。这一点可以从共享包的挑战类型定义中得到印证:在 challenge-types.ts 中,dailyChallengeJs的编号正是 28(dailyChallengePy为 29),并由此派生了以下工具函数:
getIsDailyCodingChallenge(challengeType):判断某个挑战类型是否属于每日编程挑战(仅dailyChallengeJs与dailyChallengePy两种);getDailyCodingChallengeLanguage(challengeType):把挑战类型映射为语言标识,本题即映射为javascript;- 在「测试代码存放位置」的映射表中,
dailyChallengeJs指向tests,即题目内置的断言测试就是判分依据。
从课程块的结构文件 daily-coding-challenges-javascript.json 还可以看到该块的运行特征:
challengeOrder中 Challenge 15 恰好排在 "Challenge 14: Character Battle" 与 "Challenge 16: Reverse Parenthesis" 之间,整块包含 200 道以上的挑战;usesMultifileEditor: true:编辑器支持多文件;blockLayout: "legacy-challenge-list":以传统列表形式浏览题目;helpCategory: "JavaScript":帮助分类归属 JavaScript。
在平台的运行链路上,这类题目由 tools/daily-challenges 提供的 seed 脚本从 "Dev Playground" 超块读取挑战并写入数据库的DailyCodingChallenges集合;前端展示侧由 api/src/daily-coding-challenge 下的路由提供每日挑战的查询接口,而挑战的提交判分仍走主 API。简言之,本文的这道题是一道典型的「断言驱动」编程题:通过仓库内嵌的assert测试即可完整验证你的实现。
题目规格:四条转换规则
原题给出的任务描述是:给定一个字符串,按以下规则返回它的驼峰命名(camel case)版本:
- 单词之间的分隔符是空格()、短横线(
-)、下划线(_)三类字符,连续的任意多个分隔符都视为一个单词边界("Treat any sequence of these as a word break"); - 第一个单词整体转为小写;
- 其后每个单词首字母大写、其余字母小写;
- 所有空格和分隔符都要被移除。
这四条规则里最容易被忽略的是第 1 条:分隔符不是「单个字符」而是「一个序列」。比如输入peg__leg----and,__、----各自只算一个边界,不能把连续的-当作多个单词之间的额外空词处理——否则 join 之后会产生多余的字母或大小写异常。
官方测试用例:判分依据
原题目共给出 5 组断言用例,覆盖了从简单到复杂的典型输入,这也是本仓库对该题判分的完整依据(# --hints--段):
// 基础用例:单个空格分隔 assert.equal(toCamelCase("hello world"), "helloWorld"); // 大小写规整:全大写输入需先统一小写再按规则转换 assert.equal(toCamelCase("HELLO WORLD"), "helloWorld"); // 混合分隔符:空格与短横线并存 assert.equal(toCamelCase("secret agent-X"), "secretAgentX"); // 大小写混杂:中间词部分小写 assert.equal(toCamelCase("FREE cODE cAMP"), "freeCodeCamp"); // 压力用例:连续分隔符(__、-、----、_ _ 等)混合出现 assert.equal( toCamelCase( "ye old-_-sea faring_buccaneer_-_with a - peg__leg----and a_parrot_ _named- _squawk" ), "yeOldSeaFaringBuccaneerWithAPegLegAndAParrotNamedSquawk" );最后一组是最关键的边界用例:_ _(下划线+空格+下划线)这种「跨类型连续分隔符」必须折叠为一个边界;a - peg__leg中间夹着空格的孤立-也要正确归位。若实现只对单一字符分隔(如直接split(' ')或逐字符判断),此用例必然失败。
题目的种子代码(# --seed--段)是一个待实现的空壳:
function toCamelCase(s) { return s; }官方解法逐行解析
原题目附带的参考实现(# --solutions--段)如下:
function toCamelCase(s) { const words = s.replace(/[_\- ]+/g, ' ').split(' '); return words.map((word, i) => { if (i === 0) { return word.toLowerCase(); } else { const tempWord = word.split(''); return tempWord.shift().toUpperCase() + tempWord.join('').toLowerCase(); } }).join('') }可以把它拆解为三个阶段:
阶段一:分隔符归一化。s.replace(/[_\- ]+/g, ' ')用一个正则把「一个或多个空格/短横线/下划线」统一替换为单个空格。正则[_\- ]+中:
- 字符类
[_\- ]同时收录空格、-(在字符类内\-\为防御性转义)、_; - 量词
+落实了题目第 1 条规则——任意长度的分隔符序列只算一个边界; - 全局标志
g保证整串所有边界都被替换,而不是只处理第一个。
归一化之后字符串里只剩下「单词 + 单个空格」两种成分,split(' ')就能安全地切出单词数组。由于压力用例中没有前导/尾部分隔符,words中不会出现空串元素。
阶段二:逐词大小写规整。words.map((word, i) => ...)利用索引区分首词与其余词:
i === 0时直接word.toLowerCase(),对应规则 2;- 其余词用
word.split('')拆成字符数组,shift()取出首字符并toUpperCase(),剩余字符join('')后toLowerCase(),再拼接——对应规则 3。这里shift()的妙处在于首字符不参与「其余转小写」的逻辑,避免大小写处理重复或遗漏。
阶段三:拼接。join('')以空字符串连接所有词,分隔符在阶段一已经被替换掉,因此天然满足规则 4。
实现要点与常见错误
结合仓库内的测试用例与解法,实现时值得注意的要点有:
- 先归一化、再拆分。如果直接
split单个字符(例如split(/[_\- ]/)),连续分隔符会产生空串元素,首词判断(i === 0)之后还会把空串当作「后续单词」处理,导致输出多出一个被大写的空串或大小写错位。 - 大小写处理要覆盖整词而非首字符之外的一小部分。
"FREE cODE cAMP"这类输入中,后续词的首字母可能本身已是小写(cODE),因此必须shift()后toUpperCase(),而不能假设首字母已是小写。 - 首词也要完整小写。
"HELLO WORLD"中首词全大写,仅处理首字母大写/小写都不够,必须对整个首词调用toLowerCase()。 - 等价写法:也可以用
s.match(/[^_\- ]+/g)直接提取出「非分隔符」的单词序列,跳过显式 replace;但仓库给出的官方解法以 replace + split 为主干,语义上与题目「一个序列 = 一个边界」的表述逐字对应,可读性更贴近题面规则。
在仓库中复核本题
如果你在本仓库中查看或复核这道题,推荐的路径是:
- 阅读题目原文与全部用例:curriculum/challenges/english/blocks/daily-coding-challenges-javascript/6821ebd4237de8297eaee791.md;
- 确认它在课程块中的排序与编辑器配置:curriculum/structure/blocks/daily-coding-challenges-javascript.json;
- 确认
challengeType: 28的语义(每日 JavaScript 挑战、语言映射、测试位置):packages/shared/src/config/challenge-types.ts; - 了解每日挑战数据如何进入平台:tools/daily-challenges 的 seed 脚本说明,以及 api/src/daily-coding-challenge 中的查询路由说明。
把本文的解法粘贴进任意 Node 环境,运行题目中给出的 5 条assert.equal断言即可完整验证实现——这组断言同时也是该题在 freeCodeCamp 平台上的判分测试。
【免费下载链接】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),仅供参考