首页
/ freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Consonant Case 字符串转换(Challenge 163)

freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Consonant Case 字符串转换(Challenge 163)

2026-09-08 21:40:33作者:齐添朝

freeCodeCamp 开源仓库中的「每日编程挑战」(Daily Coding Challenges)为学习者提供了一年 365 道短小精悍的算法练习题。本文聚焦其中的 Challenge 163: Consonant Case,完整讲解题目规则、hints 测试断言、参考解法与多种实现思路,并基于仓库源码剖析这道题从课程 Markdown 到线上答题系统的完整流转链路(schema 校验 → GraphQL 抓取 → 数据库 seed → API 下发),让读者既能独立解出题目,也能理解它在 freeCodeCamp 技术栈中的真实位置。

题目速览:Challenge 163 的核心规则

题目位于仓库的 daily-coding-challenges-javascript 挑战块challengeType: 28 属于每日编程挑战类型。题目要求实现函数 toConsonantCase(str),将传入的「变量名」字符串按以下三条规则转换:

  1. 所有辅音字母转为大写(consonant → uppercase);
  2. 所有元音字母(aeiou,不区分大小写)转为小写
  3. 所有连字符 - 转为下划线 _

题目对除字母和连字符之外的其他字符(如 _~、数字、符号)没有提出任何转换要求——它们应当原样保留,这正是该题容易被忽略的隐藏约束,也是官方参考解法中最值得学习的细节。

题目同时存在于 JavaScript 与 Python 两个语言版本的挑战块(见 dev-playground.json,其中并列定义了 daily-coding-challenges-javascriptdaily-coding-challenges-python),两道同题号题目的标题与描述必须完全一致(在 helpers.ts 中有专门的校验逻辑)。

读懂 hints:四个测试断言的含义

题目以 # --hints-- 形式给出了 4 个 assert.equal 断言,它们既是评判依据,也精确划定了函数的行为边界。逐条分析:

输入 期望输出 考察点
"helloworld" "HeLLoWoRLD" 全小写输入:辅音大写、元音小写
"HELLOWORLD" "HeLLoWoRLD" 全大写输入:先做大小写归一化
"_hElLO-WOrlD-" "_HeLLo_WoRLD_" 大小写混合 + 连字符转下划线
"_~-generic_~-variable_~-name_~-here-~_" "_~_GeNeRiC_~_VaRiaBLe_~_NaMe_~_HeRe_~_" 特殊字符 _~ 原样保留,连字符全部转换

最后一个用例揭示了最关键的行为细节:_~-generic_~-... 转换后变成 _~_GeNeRiC_~_...——输入中 _~ 之后紧跟 -,输出里 _~ 保持原样而 - 变为 _,字符串整体长度不变,只发生了字符级替换。函数必须对字符串进行逐字符处理,而不是基于单词分割或正则整体替换,否则很容易在特殊字符边界上出错。

这些 hint 最终会被解析为 tests 数组写入数据库。根据 challenge-schema.js 的定义,tests 数组中的每个元素是 { text, testString } 结构——text 是给人看的说明文字,testString 是真正在浏览器中执行断言用的代码字符串,也就是上文中 assert.equal(...) 那几行。

从零实现:官方参考解法逐行拆解

题目给出的参考解法(# --solutions--)如下:

function toConsonantCase(str) {
  const vowels = "aeiouAEIOU";
  let result = "";

  for (let char of str) {
    if (char === "-") {
      result += "_";
    } else if (/[a-zA-Z]/.test(char)) {
      if (vowels.includes(char)) {
        result += char.toLowerCase();
      } else {
        result += char.toUpperCase();
      }
    } else {
      result += char;
    }
  }

  return result;
}

逐行拆解其设计逻辑:

  • const vowels = "aeiouAEIOU":元音集合同时包含大小写。这样后续只需判断「当前字符是否在元音集合中」,无需先归一化大小写。
  • for (let char of str)for...of 逐字符迭代,天然满足「非字母、非连字符字符原样保留」的要求。这是解法正确性的关键——它按字符索引遍历,而不是按单词或按空白分割。
  • 分支一 char === "-":先处理连字符,直接拼接 _
  • 分支二 /[a-zA-Z]/.test(char):用正则判定是否为英文字母,是字母且非元音 → toUpperCase() 大写;是元音 → toLowerCase() 小写。
  • 分支三(隐式 else):数字、下划线、波浪号、空格等其他字符直接原样拼接。
  • return result:返回拼接结果,不修改原字符串(函数无副作用)。

该解法的核心思想可以概括为**「先判类型,再定大小写」**:先区分三类字符(连字符 / 字母 / 其他),再在字母类中区分元音与辅音。分支顺序决定了语义——连字符优先于字母判断,而字母判断又优先于「原样保留」,任何字符都必然落入恰好一个分支。

思路拓展:三种等价实现对比

除了官方解法,这道题还有多种等价写法。下面给出两种,便于理解题目的本质与取舍:

方案一:字符编码区间判断(避免正则)

function toConsonantCase(str) {
  const vowels = new Set("aeiouAEIOU");
  let result = "";
  for (const char of str) {
    const isLetter = /^[a-z]$/i.test(char);
    if (char === "-") {
      result += "_";
    } else if (isLetter && !vowels.has(char)) {
      result += char.toUpperCase();
    } else if (isLetter && vowels.has(char)) {
      result += char.toLowerCase();
    } else {
      result += char;
    }
  }
  return result;
}

方案二:正则整体替换(注意边界陷阱)

function toConsonantCase(str) {
  return str
    .replace(/-/g, "_")
    .replace(/[aeiou]/gi, m => m.toLowerCase())
    .replace(/[b-df-hj-np-tv-z]/gi, m => m.toUpperCase());
}

方案二代码最简洁,但有一个需要注意的缺陷:/[b-df-hj-np-tv-z]/gi 这类辅音区间写法依赖 ASCII 码表连续性(b–d、f–h、j–n、p–t、v–z 的间隔恰好跳过元音),虽然对本题目字母集有效,却容易在改造成其他字符集时出错。相比之下,逐字符扫描 + 显式元音集合的方案可读性与可维护性最佳,这也是官方选择它的原因。

复杂度分析:三种方案的时空复杂度均为 O(n)(n 为输入字符串长度)。for...ofincludes/Set.has 都是线性扫描;若追求极端性能,可将元音集合换为 Set 使单字符查询摊还 O(1),但对普通变量名长度而言差异可忽略。

解出题目后:它如何进入每日答题系统

这道 Challenge 163 并不是孤立的一道题,它是「每日编程挑战」产品功能的一环。仓库中从课程文件到线上答题的完整链路如下:

  1. 课程 Markdown 是唯一内容源:题目描述、hints、seed 代码、官方解法全部定义在 694596b0585c11170ac7c7fb.md 中,并通过 challenge-schema.js 做结构校验(challengeType 取值 0–33、tests 数组必须含 texttestString 等)。
  2. Gatsby GraphQL 抓取dev-playground 超级块(superblock)下定义了 JavaScript 与 Python 两个挑战块。种子脚本通过 fetchChallenges 向运行中的 client 的 ___graphql 端点发起 GraphQL 查询,按 superBlock: "dev-playground"block: "daily-coding-challenges-${language}" 过滤并按 challengeOrder 排序抓取全部挑战。
  3. JS 与 Python 配对合并combineChallenges 将同题号的 JS/Python 挑战合并为一条记录——题号取自数组下标(challengeNumber: i + 1,故 Challenge 163 即第 163 条),title 去掉 "Challenge 163: " 前缀,描述经 removeSection 去掉解析器添加的 <section> 包装,测试与代码文件则按语言分别挂到 javascript / python 字段。
  4. 按天排期写入 MongoDBseed-daily-challenges.ts2025-08-11 为起始日,每道题向后顺延一天(START_DATE.getTime() + i * ONE_DAY_IN_MS),脚本还硬编码了 EXPECTED_CHALLENGE_COUNT = 365 用于数量自检,并冻结起始日期防止发布后误改。因此 Challenge 163 对应的自然日是 2025-08-11 + 162 天
  5. API 按日期下发:API 层提供 daily-coding-challenge 路由,包括 /daily-coding-challenge/date/:date(按 YYYY-MM-DD 查询)、/day/:day(按 MM-DD 查询)、/today/month/:month/all/newest 六个公开 GET 接口。所有查询都以「美国中部时区的今天」为截止边界,未来日期的题目不会提前泄露——Challenge 163 也要到其排期日期当天才可访问。
  6. 响应结构与客户端消费:单题响应结构在 daily-coding-challenge.ts 的 schema 中定义,包含 iddatechallengeNumbertitledescription 以及 javascript / python 两组 { tests, challengeFiles }。客户端侧 widget.tsxcalendar.tsx 负责展示与日历导航,helpers.ts 则提供 getTodayUsCentralformatDateformatDisplayDate 等日期工具——所有「今天」均以 America/Chicago 时区计算。

从这条链路可以看出:学习者答题时填写的 toConsonantCase 实现会被浏览器中的测试运行器执行,而其依据正是题目 Markdown 中那 4 条 assert.equal;而「今天该做哪道题」则由种子脚本的日期排期与 API 的时区边界共同决定。

测试驱动验证:确保实现通过全部断言

完成实现后,可以把它直接放进浏览器控制台或 Node.js 环境,用题目提供的 4 个断言验证:

// 粘贴你的实现
function toConsonantCase(str) { /* ... */ }

// 运行题目 hints
const cases = [
  ["helloworld", "HeLLoWoRLD"],
  ["HELLOWORLD", "HeLLoWoRLD"],
  ["_hElLO-WOrlD-", "_HeLLo_WoRLD_"],
  ["_~-generic_~-variable_~-name_~-here-~_", "_~_GeNeRiC_~_VaRiaBLe_~_NaMe_~_HeRe_~_"]
];

let passed = 0;
for (const [input, expected] of cases) {
  const actual = toConsonantCase(input);
  const ok = actual === expected;
  console.log(`${ok ? "PASS" : "FAIL"} toConsonantCase(${JSON.stringify(input)}) => ${JSON.stringify(actual)}`);
  if (ok) passed++;
}
console.log(`${passed}/${cases.length} tests passed`);

全部 4 条通过即说明实现满足题目约束。在此基础上可以再做一轮自测,验证边界行为:

  • 空字符串toConsonantCase("") 应返回 ""for...of 对空串不产生任何迭代)。
  • 无字母输入toConsonantCase("-_~123") 应返回 "__~123"——连字符被替换,其余原样。
  • 非英文字母:带重音的字符(如 é)不属于 [a-zA-Z],应原样保留。

小结

Challenge 163 是一道典型的「字符级转换」题目,其考点集中于三点:大小写归一化(无论输入是全大写、全小写还是混合)、字符分类处理(辅音 / 元音 / 连字符 / 其他四类互斥分支)以及对非字母字符的保真。官方参考解法用「元音集合 + 正则判定 + 逐字符遍历」给出了简洁可靠的实现;而将这道题放回仓库语境中,它又展示了 freeCodeCamp 内容与工程体系的联动方式——一份 Markdown 定义题目,GraphQL 抓取配对,种子脚本排期入库,API 按时下发,最终在浏览器中完成一次真实的算法训练。

如果你还想继续练习同系列题目,仓库中同一挑战块还包含 Challenge 15: camelCase6821ebce237de8297eaee790.md)、Challenge 114: Camel to Snake69162d64f96574d9bb629eff.md)、Challenge 140: SCREAMING_SNAKE_CASE69272dcf1c24b44fd79137c3.md)等同属命名规范转换主题的练习题,它们的解题思路与本题目一脉相承。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525