freeCodeCamp 每日编程挑战解析:Challenge 83 Signature Validation(签名校验算法实现)
【免费下载链接】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)第 83 题展开,完整讲解「Signature Validation(签名校验)」一题的编码规则、验证目标、官方测试用例与源码级解法。你将掌握如何用 JavaScript 实现一套基于字母数值求和的签名校验函数,理解charCodeAt与for...of在字符遍历中的应用,并了解该挑战在 freeCodeCamp 课程仓库中的落地形态(Markdown 挑战文件、测试驱动校验与 Python 对照实现)。
挑战背景:它来自哪里
本挑战位于课程仓库的daily-coding-challenges-javascript块(block)内,源文件为 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68e39ed6106dac2f0a98fd65.md,文件头部元信息:
id: 68e39ed6106dac2f0a98fd65 title: "Challenge 83: Signature Validation" challengeType: 28 dashedName: challenge-83其中challengeType: 28在 packages/shared/src/config/challenge-types.ts 中被定义为dailyChallengeJs,即「每日 JavaScript 挑战」专用类型;该挑战同时存在同 id 的 Python 版本(challengeType: 29),对应文件为 curriculum/challenges/english/blocks/daily-coding-challenges-python/68e39ed6106dac2f0a98fd65.md,两者共用同一套题目描述与测试数据。块的元信息由 curriculum/structure/blocks/daily-coding-challenges-javascript.json 维护,其中记录了本块共 248 余道挑战的challengeOrder,Challenge 83 前后分别是「Challenge 82: SpOoKy~CaSe」与「Challenge 84: Infected」。
题目描述:什么是签名校验
给定三个输入:
- 一个消息字符串
message; - 一个密钥字符串
key; - 一个签名数字
signature;
需要判断该签名是否有效。这里的「签名」并非密码学意义上的哈希签名,而是一套简单编码规则下的数值总和:
- 消息与密钥中的字母各有对应数值:
a到z(小写)分别取值1到26;A到Z(大写)分别取值27到52。
- 其余所有字符(数字、标点、空格等)不产生任何数值(值为 0)。
- 签名的计算方式为:消息所有字符数值之和 + 密钥所有字符数值之和。
最终只需比对「计算出的签名」与「提供的签名」是否相等即可。
这一规则恰好对应字母在 Unicode 字符集中的连续排布:小写字母从a(97)到z(122),大写字母从A(65)到Z(90),因此可以通过码点差值把字母映射为 1–26 / 27–52 的整数,这正是后续解法的核心。
手动推演:官方示例"foo"与"bar"
题目给出的示例可以完整拆解为:
f (6) + o (15) + o (15) = 36 b (2) + a (1) + r (18) = 21 36 + 21 = 57即:
f是第 6 个小写字母 →6;o是第 15 个小写字母 →15,消息"foo"合计36;b→2,a→1,r→18,密钥"bar"合计21;- 两者相加得到签名
57,因此verify("foo", "bar", 57)返回true。
官方测试用例(hints)
原文档共给出 6 个断言,覆盖了大小写混合、标点空格混入、长文本等场景:
| 调用 | 期望结果 | 说明 |
|---|---|---|
verify("foo", "bar", 57) | true | 官方示例,全部小写 |
verify("foo", "bar", 54) | false | 签名差 3,验证不通过 |
verify("freeCodeCamp", "Rocks", 238) | true | 大小写混合 |
verify("Is this valid?", "No", 210) | false | 含空格与标点,需忽略非字母 |
verify("Is this valid?", "Yes", 233) | true | 同一消息、不同密钥与签名 |
verify("Check out the freeCodeCamp podcast,", "in the mobile app", 514) | true | 长文本 + 逗号等标点 |
其中第 4、5 条尤其能检验对「非字母字符值为 0」这一规则的理解:消息"Is this valid?"中含 3 个空格、1 个问号与 2 个单词边界,计算时必须全部跳过,只累加字母。例如第 5 条中"Is this valid?"的各字母值之和为I(35) + s(19) + t(20) + h(8) + i(9) + s(19) + v(22) + a(1) + l(12) + i(9) + d(4) = 158,"Yes"为Y(51) + e(5) + s(19) = 75,合计233,正好命中。
测试断言本身以 Chai 风格写入挑战文件的--hints--段,例如:
assert.isTrue(verify("foo", "bar", 57)); assert.isFalse(verify("foo", "bar", 54));起始代码(seed)
挑战要求基于以下函数骨架补全实现,verify接收三个参数并返回布尔值:
function verify(message, key, signature) { return message; }注意初始代码直接return message(返回一个字符串),必然无法通过任何断言,这是有意为之——课程测试基建会验证「初始代码必须让测试失败」,具体见后文「测试如何验证你的代码」。
官方解法逐行拆解(JavaScript)
--solutions--段给出的参考答案将问题拆成三个层次:单字符求值、字符串求和、签名比对。
function verify(message, key, signature) { function charValue(ch) { if (ch >= 'a' && ch <= 'z') return ch.charCodeAt(0) - 'a'.charCodeAt(0) + 1; if (ch >= 'A' && ch <= 'Z') return ch.charCodeAt(0) - 'A'.charCodeAt(0) + 27; return 0; } function computeSum(str) { let sum = 0; for (let ch of str) { sum += charValue(ch); } return sum; } const total = computeSum(message) + computeSum(key); return total === signature; }关键实现点:
- 字符数值映射:
ch.charCodeAt(0)返回字符的 Unicode 码点。'a'.charCodeAt(0)为 97,因此charCodeAt(0) - 97 + 1把a映射为 1、z映射为 26;大写分支以'A'(65)为基准再加 27,得到 27–52。字符串比较ch >= 'a' && ch <= 'z'能正确判定单个字母字符的区间。 - 逐字符求和:
for...of按 Unicode 码点顺序遍历字符串中的每个字符(对本题的 ASCII 输入等价于逐字符遍历),把每个字符的charValue累加。非字母字符落入return 0分支,自然被忽略。 - 严格相等比对:
total === signature用严格相等运算符,避免隐式类型转换引入的意外结果;签名以数字类型传入,total亦为数字,二者类型一致。
复杂度分析:假设m为消息长度、k为密钥长度,算法对每个字符恰好访问一次,时间复杂度为O(m + k);仅使用常数级额外空间,空间复杂度为O(1)。
Python 对照解法
同一挑战的 Python 版本(curriculum/challenges/english/blocks/daily-coding-challenges-python/68e39ed6106dac2f0a98fd65.md)逻辑完全一致,仅将charCodeAt换成ord:
def verify(message, key, signature): def charValue(ch) -> int: if 'a' <= ch <= 'z': return ord(ch) - ord('a') + 1 elif 'A' <= ch <= 'Z': return ord(ch) - ord('A') + 27 else: return 0 def compute_sum(s): return sum(charValue(ch) for ch in s) total = compute_sum(message) + compute_sum(key) return total == signaturePython 版本利用生成器表达式sum(charValue(ch) for ch in s)一步完成求和,测试断言则通过runPython内嵌unittest.TestCase().assertIs(...)实现,例如:
({test: () => { runPython(` from unittest import TestCase TestCase().assertIs(verify("foo", "bar", 57), True)`) }})仓库如何验证你的代码:测试基建剖析
该挑战的测试并非只在网页端运行,课程仓库本身也对其做了一整套自动化验证,可以从 curriculum/src/test/daily-challenges.test.js 看到:
- 测试会加载
dev-playground超级块下所有挑战,并按 block 过滤出daily-coding-challenges-javascript与daily-coding-challenges-python; - 断言两个语言版本挑战数量一致(每题都有 JS 与 Python 双版本);
- 逐一比对同下标挑战的
id、title、description与测试条数,确保双语版本严格对齐(当前语言非英文时跳过标题/描述比对)。
而 curriculum/src/test/test-challenges.js 中的通用流程对每个挑战执行两项关键检查:
- 初始内容必须让测试失败:把挑战文件的
--seed-contents--(此处即return message;骨架)作为考生代码跑一遍官方 hints,断言其结果必须是失败的,防止题目答案被误写进种子代码; - 参考答案必须通过全部测试:将
--solutions--段替换进代码后重跑同一组测试断言,保证官方解法与 hints 完全自洽。
也就是说,Challenge 83 的 6 条断言会同时被用来「拒绝骨架代码、接受参考解法」,形成一套闭环的挑战质量保障机制。从challengeType映射看,dailyChallengeJs使用classic类型的测试 runner(见 packages/shared/src/config/challenge-types.ts),即直接在浏览器环境中执行断言函数并检查返回值。
边界情况与易错点
结合编码规则与测试用例,实践时需特别注意:
- 大小写区分:
a(1)与A(27)数值不同,不能统一转为小写再映射,否则"Rocks"这类含大写字母的用例会算错; - 非字母字符:空格、问号、逗号、数字等一律按 0 处理,且不能被跳过导致索引错位——只需正常遍历并在求值时返回 0 即可;
- 返回值类型:函数必须返回布尔值
true/false,不能返回数字差值或字符串; - 比较运算:JavaScript 分支中
ch >= 'a' && ch <= 'z'依赖单字符与单字符的字典序比较,若对多字符子串使用会得到与预期不符的结果; - 大字符串输入:题目包含长文本用例(如最后一条含逗号的 40 字符消息),但数值规模仍在
Number安全整数范围内,无需大数处理。
延伸思考:从求和到真实校验
本题的「签名」是字母数值求和,属于教学性的简化模型。从中可以延伸思考真实签名机制的两个核心原则,它们在本题中均有雏形:
- 确定性与可复现:相同的消息 + 密钥必须计算出相同的签名,因此逐字符映射规则必须无歧义(本题用字符区间而非字符集枚举保证);
- 对篡改敏感:改动任何字母都会改变总和,从而改变签名——第 2 条测试
57 → 54正是因为求和结果与给定签名不一致而判定无效。
现实中的 HMAC、RSA 签名等方案在此模型上叠加了密钥分散、不可逆哈希与抗碰撞设计,但其「用双方共知的材料生成可验证的指纹」的思想一脉相承。
小结
Challenge 83「Signature Validation」是一道典型的基础算法题:掌握字符到数值的区间映射、字符串逐字符遍历与累计求和,即可在O(m + k)时间内完成签名校验。通过本文你不仅掌握了题目本身的解法(含官方 JS 答案与 Python 对照),还了解了它在 freeCodeCamp 课程仓库中的完整生命周期——从 挑战 Markdown 文件 的 front-matter 元信息,到 challenge-types.ts 的类型定义,再到 test-challenges.js 的「骨架必败、解答必过」双端验证机制。若想进一步探索同系列题目,可直接查看 daily-coding-challenges-javascript 块目录 下的其他挑战文件,或阅读 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),仅供参考