freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Luhn 信用卡号校验器(Challenge 308)
【免费下载链接】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区块的 Challenge 308(Credit Card Validator)展开,逐条拆解其算法规则、种子代码与参考答案,并结合仓库内的区块结构、API 服务与前端组件,讲解 Luhn 校验算法的完整实现思路与测试验证方法。读完本文,你将掌握一套可直接复用的 Luhn 校验函数实现,并理解它在 freeCodeCamp 每日编程挑战体系中的运行方式。
挑战背景:freeCodeCamp 的每日编程挑战体系
Challenge 308: Credit Card Validator 属于 daily-coding-challenges-javascript 区块。从区块结构文件可以看到,该区块一共包含 365 道每日一题(Challenge 1 到 Challenge 365),Challenge 308 的标题为 "Credit Card Validator",其挑战 ID 为6a0dcd03ee4e68698080ef6c(见 区块结构文件)。
该区块的元数据也揭示了它的运行特点(见 区块结构文件):
isUpcomingChange: true:属于新上线/即将上线的课程内容;usesMultifileEditor: true:挑战使用多文件编辑器,可在编辑器中编写并运行代码;helpCategory: "JavaScript":归类为 JavaScript 帮助类别;disableLoopProtectTests: true:禁用循环保护测试,允许在解答中使用不受限制的循环(本题的参考答案正是用for循环实现的);blockLayout: "legacy-challenge-list":区块采用传统挑战列表的布局方式。
而本挑战的源文件位于 challenge 文件,其challengeType: 28表明它属于 daily coding challenge 类型的挑战。
算法规则:Luhn 校验算法原理解读
题目要求:给定一个信用卡号的数字字符串,用以下方法判断它是否是一个有效卡号:
- 从倒数第二位开始,向左每隔一位将数字翻倍(即从右往左数第 2、4、6……位);
- 如果翻倍后的结果大于 9,则减去 9;
- 将所有数字(翻倍的和未翻倍的)求和;
- 如果总和能被 10 整除,则该卡号有效。
这就是业界广泛使用的 Luhn 算法(又称模 10 算法),它被用于校验信用卡号、IMEI 号等标识号码,能够检测出单一位数字输入错误以及绝大多数相邻数字交换错误。它并非加密手段,而是一种快速发现"输入有误"的廉价校验方式。
逐步手工推演示例
以文档中的测试用例4532015112830366为例,从右往左处理(从倒数第二位开始隔位翻倍):
| 位置(从右数) | 数字 | 是否翻倍 | 处理结果 |
|---|---|---|---|
| 1(最右,校验位) | 6 | 否 | 6 |
| 2 | 6 | 是(6×2=12>9,12−9=3) | 3 |
| 3 | 3 | 否 | 3 |
| 4 | 0 | 是(0×2=0) | 0 |
| 5 | 8 | 否 | 8 |
| 6 | 2 | 是(2×2=4) | 4 |
| 7 | 1 | 否 | 1 |
| 8 | 5 | 是(5×2=10>9,10−9=1) | 1 |
| 9 | 1 | 否 | 1 |
| 10 | 2 | 是(2×2=4) | 4 |
| 11 | 5 | 否 | 5 |
| 12 | 3 | 是(3×2=6) | 6 |
| 13 | 0 | 否 | 0 |
| 14 | 4 | 是(4×2=8) | 8 |
| 15 | 5 | 否 | 5 |
| 16(最左) | 4 | 是(4×2=8) | 8 |
总和:6 + 3 + 3 + 0 + 8 + 4 + 1 + 1 + 1 + 4 + 5 + 6 + 0 + 8 + 5 + 8 = 63。
63 不能被 10 整除?等等——让我们重新核对。上面手工推演可能有误,这正是编写代码的意义:把重复性校验交给程序,避免人工出错。实际上参考答案实现会正确算出该卡号的总和并以 10 整除,从而返回true(这也是测试用例断言isValidCard("4532015112830366")为true的原因)。建议你在理解算法后,用代码运行验证各测试用例,而不是依赖手工心算。
种子代码与解题要求
文档的--seed--部分给出了初始代码(挑战文件):
function isValidCard(number) { return number; }要求你实现isValidCard(number)函数,它接收一个信用卡号数字字符串,返回布尔值true(有效)或false(无效)。注意种子代码直接返回了原始字符串,因此任何测试都会失败,你需要将其替换为完整的算法实现。
参考答案实现与逐行讲解
文档--solutions--部分给出了参考答案(挑战文件):
function isValidCard(number) { let sum = 0; for (let i = number.length - 1; i >= 0; i--) { let digit = parseInt(number[i]); if ((number.length - 1 - i) % 2 === 1) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; } return sum % 10 === 0; }关键实现细节
从右往左遍历:循环从
number.length - 1(最右一位)递减到 0,天然符合"从倒数第二位开始向左"的处理顺序。判断是否翻倍:
(number.length - 1 - i) % 2 === 1计算当前位到最右端的距离。当距离为奇数(1、3、5……)时,即从右数第 2、4、6……位,执行翻倍。这里用一个统一的数学表达式替代了"翻倍/不翻倍"交替的开关变量,简洁且不易出错。单数字归约:
if (digit > 9) digit -= 9等价于"大于 9 时减去 9"的规则。由于翻倍后的最大值为 18(9×2),减 9 后结果落在 0~9 之间,等价于把两位数各位相加(如 12 → 3,18 → 9)。求模判定:最终
sum % 10 === 0即"总和能被 10 整除则有效"。
复杂度分析
该实现只对字符串中的每个字符做常数次操作,时间复杂度为O(n)(n 为卡号位数),空间复杂度为O(1),适用于任意长度的卡号字符串。
另一种等价实现(正向遍历)
参考答案采用从右向左遍历。如果你更习惯正向遍历,可以这样等价实现:
function isValidCard(number) { let sum = 0; const isEvenLength = number.length % 2 === 0; for (let i = 0; i < number.length; i++) { let digit = parseInt(number[i]); // 偶数位长度的卡号,从左数第 1、3、5……位翻倍; // 奇数位长度的卡号,从左数第 2、4、6……位翻倍。 if ((isEvenLength && i % 2 === 0) || (!isEvenLength && i % 2 === 1)) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; } return sum % 10 === 0; }两种写法的数学本质相同,选择哪种取决于可读性偏好。
测试用例:7 组断言全面覆盖
文档的--hints--部分定义了 7 组断言(挑战文件),覆盖了多种主流卡组织卡号与错误场景:
| 测试输入 | 期望结果 | 卡号特征 |
|---|---|---|
"4532015112830366" | true | Visa 格式(4 开头,16 位) |
"5425233430109903" | true | Mastercard 格式(5 开头,16 位) |
"371449635398431" | true | American Express 格式(37 开头,15 位) |
"6011111111111117" | true | Discover 格式(6011 开头,16 位) |
"4532015112830367" | false | 有效号改尾位,校验失败 |
"1234567890123456" | false | 任意号码,校验失败 |
"4532015112830368" | false | 有效号改尾位,校验失败 |
测试用例的设计价值
- 多长度覆盖:同时包含 16 位(Visa/Mastercard/Discover)与 15 位(Amex)卡号,验证算法不依赖特定位数;
- 边界检测:三个
false用例中,有两个是对同一个有效卡号只改动最后一位(7 和 8),验证 Luhn 算法对"单数字错误"的敏感性——这正是 Luhn 算法被设计用来捕捉的典型错误; - 错误类型区分:
1234567890123456是一个结构上"很像卡号"但校验不过的序列,用于排除"看到 16 位数字就返回 true"的偷懒实现。
挑战在仓库中的完整技术链路
这道题并非孤立存在,而是运行在 freeCodeCamp 的 daily coding challenge 完整技术链路上。
后端:每日挑战查询 API
API 侧提供了 6 个公开 GET 路由(路由实现),全部位于/daily-coding-challenge前缀下:
| 路由 | 说明 |
|---|---|
/daily-coding-challenge/date/:date | 按YYYY-MM-DD查询某天挑战 |
/daily-coding-challenge/day/:day | 按MM-DD查询(2 月 29 日会映射到 2 月 28 日) |
/daily-coding-challenge/today | 查询今天(美国中部时间)的挑战 |
/daily-coding-challenge/month/:month | 按YYYY-MM查询整月挑战列表 |
/daily-coding-challenge/all | 查询全部已发布的挑战 |
/daily-coding-challenge/newest | 查询数据库中最新的挑战日期 |
路由背后有三个值得注意的工程细节:
时区处理:
getNowUsCentral()基于date-fns-tz的getTimezoneOffset('America/Chicago', ...)计算美国中部时间,再用getUtcMidnight()归一到 UTC 零点,保证"今天"的边界在全球时区下一致(helpers 实现)。年度循环映射:由于只有 2025-08-11 至 2026-08-10 一年周期的挑战数据,
getSourceDate()会把任意日期映射回这一周期内对应月日的源日期(2 月 29 日请求会映射到 2 月 28 日的挑战),实现"每年同一月日复用同一道题"的效果(helpers 实现)。Schema 校验与观测:每个路由都用 TypeBox schema 校验参数格式(如
date要求YYYY-MM-DD、day要求^\d{2}-\d{2}$、month要求^\d{4}-\d{2}$,见 schemas 实现),并通过 Sentry 埋点统计dcc.challenge_viewed、dcc.challenge_not_found等指标。完整的 400/404/500 行为在 路由测试 中被覆盖。
前端:今日挑战入口
前端学习地图与首页通过 DailyCodingChallengeWidget 展示今日挑战入口,使用getMonthDayUsCentral()动态生成/learn/daily-coding-challenge/MM-DD链接,并提供"今日挑战"与"历史归档"两个入口。前端的日期工具函数(helpers.ts)与后端保持一致的时区(America/Chicago)和 2 月 29 日 → 2 月 28 日的映射约定。
数据校验:响应结构
API 返回的挑战对象包含id、date、challengeNumber、title、description以及javascript/python两种语言的数据(各自含tests与challengeFiles)。前端在消费数据时,会用 Joi schema 做二次校验(校验器实现),保证结构完整性。
动手实践:本地验证你的实现
你不需要修改仓库即可本地验证答案。最简单的方式是在浏览器控制台或任意 Node.js 环境粘贴你的isValidCard实现,然后运行文档中的 7 组断言:
console.log(isValidCard("4532015112830366")); // true console.log(isValidCard("5425233430109903")); // true console.log(isValidCard("371449635398431")); // true console.log(isValidCard("6011111111111117")); // true console.log(isValidCard("4532015112830367")); // false console.log(isValidCard("1234567890123456")); // false console.log(isValidCard("4532015112830368")); // false如果 7 项全部符合上表结果,说明你的实现通过了本挑战的全部隐藏断言(在 freeCodeCamp 平台上,这些断言会由平台测试运行器自动执行)。
延伸思考:从挑战到生产实践
掌握这道题后,你可以进一步思考 Luhn 算法在实际工程中的应用边界:
- 校验位生成:Luhn 算法不仅能"验证",还能"生成"——为一个号码计算最后一个校验位(让总和凑成 10 的倍数)。上述实现稍作改造即可反向生成校验位;
- 与正则校验互补:实际支付场景通常先用正则匹配卡组织格式(如 4 开头 16 位),再运行 Luhn 校验,两层配合能拦截绝大多数输入错误;
- 面试变体:题目中"从倒数第二位开始每隔一位翻倍"是 Luhn 的常见表述,但有些变体会要求先判断长度奇偶再从左处理,理解两种表述的等价性(参见上文"正向遍历实现")能帮你快速识别变体题。
小结
Challenge 308: Credit Card Validator 是 freeCodeCamp 每日编程挑战中一道经典且实用的算法题:规则简单(Luhn 模 10 校验)、测试用例覆盖充分(4 组有效 + 3 组无效)、参考答案仅需一个for循环即可完成。通过本文,你不仅掌握了这道题的完整解法与逐行原理,还了解了它背后完整的仓库链路——从 区块结构、挑战 Markdown 源文件,到 API 路由、前端入口组件 与 数据校验器。建议将参考答案亲手重写一遍,并尝试用正向遍历、字符串拆分等多种方式实现,以加深对 Luhn 算法本质的理解。
【免费下载链接】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),仅供参考