freeCodeCamp 每日编程挑战解读:用字符编码与模运算实现凯撒密码解译(Message Decoder)
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本文以 freeCodeCamp 课程仓库中 Daily Coding Challenges 系列(JavaScript)的第 12 题 "Challenge 12: Message Decoder" 为主体,完整覆盖该题的原始题目约束、四组官方测试用例与参考解答,并结合仓库中的挑战类型定义、课程结构与出题管线源码,深入讲解如何用charCodeAt/fromCharCode加模运算(% 26)实现支持正负偏移、自动环绕且保留大小写的凯撒密码解码。读完本文,你能独立写出该函数、理解偏移量取模消除的原理,并了解这道题在 freeCodeCamp 课程体系中的落地位置。
题目在仓库中的位置与出处
本题对应仓库中的课程文档 Challenge 12: Message Decoder,其 front matter 声明了以下元数据:
id: 68216ef80f957572e7c340c5 title: "Challenge 12: Message Decoder" challengeType: 28 dashedName: challenge-12challengeType: 28并非随意编号。在共享包的 challenge-types.ts 中可以看到:
const jsLab = 26; const pyLab = 27; const dailyChallengeJs = 28; const dailyChallengePy = 29;即 28 是dailyChallengeJs类型。同一文件还规定该类型的视图与提交方式:
// determine the component view for each challenge export const viewTypes = { ... [dailyChallengeJs]: 'classic', // 使用经典编辑器视图 ... }; // determine the type of submit function to use on completion export const submitTypes = { ... [dailyChallengeJs]: 'tests', // 通过跑断言测试判定通过 ... };从源码结构看,dailyChallengeJs会被getIsDailyCodingChallenge(同文件 L187-L188)识别为每日编程挑战,并用viewTypes中的classic视图在客户端渲染,完成判定走tests提交路径——也就是说,本文后面的四组assert.equal用例就是判题依据本身。
该题的排序信息记录在 block 结构文件(dashedName: daily-coding-challenges-javascript)中:68216ef80f957572e7c340c5排在 "Challenge 11: Mile Pace" 之后、"Challenge 13: Unnatural Prime" 之前,title为 "Challenge 12: Message Decoder"。该结构文件同时声明了usesMultifileEditor: true、helpCategory: "JavaScript"、blockLayout: "legacy-challenge-list"等块级配置。
值得注意的一点:该 block 配置中isUpcomingChange: true,且 api/src/daily-coding-challenge 的 README 说明每日挑战的信息接口独立于主 API——从目录结构(routes/、schemas/、utils/)推断,线上"每日挑战"是按日期轮播投放的挑战集,而本仓库中以固定文件形式存放的是题库本体。因此以下讲解以题库中的题目文件为准。
题目原文:给定密文与偏移量,还原明文
题目的原始描述(description 小节)为:
Given a secret message string, and an integer representing the number of letters that were used to shift the message to encode it, return the decoded string.
- A positive number means the message was shifted forward in the alphabet.
- A negative number means the message was shifted backward in the alphabet.
- Case matters, decoded characters should retain the case of their encoded counterparts.
- Non-alphabetical characters should not get decoded.
翻译成工程语言,函数签名是decode(message: string, shift: number): string,约束有四条:
shift为正数:编码时字母在字母表中向前移动了shift位(如a → e);shift为负数:编码时字母向后移动了|shift|位;- 大小写必须保留:编码字符是大写,解码结果就是大写,反之亦然;
- 非字母字符原样透传:空格、标点、数字等一律不做变换。
这本质上是一个双向凯撒密码:编码是"字母平移",解码就是"反向平移"。
官方测试用例与断言
题目文档的 hints 小节给出了四组官方断言,它们同时覆盖了正偏移、大偏移、负偏移、混合大小写和符号透传等关键场景:
输入message | 输入shift | 期望输出 | 覆盖点 |
|---|---|---|---|
"Xlmw mw e wigvix qiwweki." | 4 | "This is a secret message." | 正偏移、含句号 |
"Byffi Qilfx!" | 20 | "Hello World!" | 大偏移(20 等价于 -6) |
"Zqd xnt njzx?" | -1 | "Are you okay?" | 负偏移、含问号 |
"oannLxmnLjvy" | 9 | "freeCodeCamp" | 混合大小写、无空格 |
对应的断言写法(即判题时执行的testString内容):
assert.equal(decode("Xlmw mw e wigvix qiwweki.", 4), "This is a secret message."); assert.equal(decode("Byffi Qilfx!", 20), "Hello World!"); assert.equal(decode("Zqd xnt njzx?", -1), "Are you okay?"); assert.equal(decode("oannLxmnLjvy", 9), "freeCodeCamp");种子代码(seed-contents)是一个只返回原文的占位实现:
function decode(message, shift) { return message; }解题目标就是替换return message;,让上述四个断言全部通过。
核心原理:字符编码差值 + 取模消除
字母在 Unicode 中是连续区间
ASCII/Unicode 中,'A'到'Z'、'a'到'z'各是一个连续编号区间('A'的码位是 65,'a'是 97)。因此字母可以无损地映射为 0–25 的"槽位":
const base = char === char.toLowerCase() ? 'a'.charCodeAt(0) : 'A'.charCodeAt(0); // base 为 97(小写)或 65(大写) const slot = char.charCodeAt(0) - base; // 0 表示 a/A,25 表示 z/Z编码与解码互为反向平移
设明文字母的槽位为p,编码后的槽位为c,则:
- 正偏移编码:
c = (p + shift) % 26,所以解码要减去shift:p = (c - shift) mod 26 - 负偏移编码:
c = (p - |shift|) % 26,解码要加上|shift|——同样可写成(c - shift) mod 26(此时shift为负,减负等于加正)
统一公式就是p = (c - shift) mod 26,这正是题目文档中官方解答采用的形式:
const offset = (charCode - base - shift + 26) % 26;这里有一个 JavaScript 特有的关键细节:%取余保留被除数的符号,负数取余会得到负值(如(-3) % 26 === -3)。因此在减shift之后先+ 26,保证表达式落为非负数再取模,从而得到 0–25 的合法槽位。+ 26足以覆盖单个字母一次平移的边界,因为c - shift ∈ (-26, 26)区间内时加一次 26 必为非负。
延伸:若要支持任意大小的
shift(例如shift = 100),需要先把shift归一化:shift = ((shift % 26) + 26) % 26。题目给出的shift = 20在 (-26, 26) 内,所以官方解答无需这一步。
参考解答逐行拆解
题目文档 solutions 小节给出的官方解答:
function decode(message, shift) { return message.split('').map(char => { if (/[a-zA-Z]/.test(char)) { const base = char === char.toLowerCase() ? 'a'.charCodeAt(0) : 'A'.charCodeAt(0); const charCode = char.charCodeAt(0); const offset = (charCode - base - shift + 26) % 26; return String.fromCharCode(base + offset); } else { return char; } }).join(''); }逐行说明:
message.split(''):把字符串拆成单字符数组,配合map对每个字符独立处理——这是"逐字符变换 + 重组"的函数式惯用写法;/[a-zA-Z]/.test(char):只处理纯 ASCII 字母。非字母(空格、.、!、?等)走else分支原样返回,满足约束 4。题目数据只含 ASCII 字母,所以用[a-zA-Z]而非\p{L}是精确且安全的;char === char.toLowerCase():判断大小写并选定基准码base(97 或 65)。这一步保证解码后的字符与密文字符大小写一致,满足约束 3;(charCode - base - shift + 26) % 26:核心槽位计算,如上一节所述;String.fromCharCode(base + offset):把槽位映射回字符,大小写由base决定;.join(''):把字符数组拼回字符串。
用第一组用例手工验证一下shift = 4时的'X':
'X'.charCodeAt(0)= 88,base= 65,槽位c= 23(X 是第 24 个字母);offset = (23 - 4 + 26) % 26 = 45 % 26 = 19;String.fromCharCode(65 + 19)='T'。✓
负偏移用例'Z'(shift = -1):offset = (25 - (-1) + 26) % 26 = 52 % 26 = 0→'A',即"Zqd" → "Are"。✓
大偏移用例shift = 20的'B':offset = (1 - 20 + 26) % 26 = 7 % 26 = 7→'H'。✓ 可见 20 位的"前移编码"等价于解码时"后移 6 位",模 26 的周期性让两者完全等价。
验证思路与等价写法
由于submitTypes[dailyChallengeJs] === 'tests',本题的通过标准就是四组assert.equal全部为真。自行验证时可以直接把测试用例粘到 Node 或浏览器控制台跑一遍;仓库中同类挑战的数据在入库时还会经过客户端侧的 Joi 结构校验(参见 daily-coding-challenge-validator.ts,其中tests数组要求每项必须含text与testString两个字段,testString正是上述assert.equal(...)字符串),这从管线角度印证了"断言即判题"的设计。
在等价实现层面,还有几种常见变体,供对照理解:
- 查表法:预构造
'abcdefghijklmnopqrstuvwxyz'字符串,用indexOf定位槽位,再按下标取字符; - 正则替换法:
message.replace(/[a-zA-Z]/g, fn),跳过显式split/map/join; - 先归一化 shift:
shift = ((shift % 26) + 26) % 26,之后解码公式统一为(slot - shift + 26) % 26。
三者与官方解答在题目给定的输入域(ASCII 字母 + 整数 shift)内行为一致;官方解答的优势在于不引入额外数据结构、一行表达式完成核心计算。
小结
这道 Message Decoder 是 freeCodeCamp JavaScript 每日挑战题库中的一个典型字符串处理题,其教学价值集中在三个可迁移的技能点:用charCodeAt/fromCharCode在字符与数字之间自由转换、用% 26实现字母表环绕、以及正确处理 JavaScript 取余的负数语义。题目文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68216ef80f957572e7c340c5.md,挑战类型与判题方式定义在 packages/shared/src/config/challenge-types.ts,块级排序与编辑器配置见 curriculum/structure/blocks/daily-coding-challenges-javascript.json,可按此路径继续深入研究该题库的组织方式。
【免费下载链接】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),仅供参考