freeCodeCamp 每日编程挑战第 184 题解析:用 JavaScript 计算越野滑雪相对成绩(Cross-Country Skiing)
【免费下载链接】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模块(Winter Games 冬季运动会系列第 5 天),完整拆解 Challenge 184「Cross-Country Skiing(越野滑雪)」:给定一组按快慢排序的"H:MM:SS"格式完赛时间,要求计算每位选手落后于冠军的相对成绩,并以"+M:SS"格式输出。通过阅读本文,你将掌握「字符串时间 → 秒数 → 差值 → 格式化回读」的完整解题链路、逐条对照的测试用例分析,以及该挑战在 freeCodeCamp 课程与 API 体系中的实际运行机制。
一、挑战背景:Winter Games 系列与每日编程挑战
Challenge 184 是 freeCodeCamp 每日编程挑战(Daily Coding Challenge)中的一道题,隶属于daily-coding-challenges-javascript代码块。从 模块结构定义 可以看出,该模块包含 200+ 道编号挑战,其中 Challenge 180 至 Challenge 196 构成「Winter Games(冬季运动会)」主题系列,涵盖雪板、冬季两项、跳台滑雪、越野滑雪、花样滑冰、速度滑冰、雪橇、俯式冰橇、高山滑雪、冰球、钢架雪车、北欧两项、登山滑雪、自由式滑雪、冰壶与闭幕日等场景。Challenge 184 即「Winter Games Day 5: Cross-Country Skiing」,id 为697a49e6ff50d756c9b69361,题目类型challengeType: 28(每日编码挑战专用类型)。
模块配置中还标注了usesMultifileEditor: true(支持多文件编辑器)与helpCategory: "JavaScript"(归入 JavaScript 帮助分类),说明这道题在平台上以 JavaScript 在线编程挑战的形式呈现,配有多组断言测试(hints)供浏览器端实时校验。
二、题目定义与输入输出格式
2.1 任务描述
原题要求实现函数getRelativeResults(results):给定一场越野滑雪比赛的完赛时间数组,将其转换为「落后冠军的时间差」。
题目给出了四条明确的输入/输出约束:
- 给定的时间是以
"H:MM:SS"格式表示的字符串; - 给定时间按从快到慢排序(数组第一项即冠军);
- 冠军(最快时间)应输出为字符串
"0"; - 其余每个时间应输出「落后冠军的时间」,格式为
"+M:SS"。
2.2 官方示例
题目给出的示例输入输出:
getRelativeResults(["1:25:32", "1:26:10", "1:27:05"]); // 返回 ["0", "+0:38", "+1:33"]手工核对:冠军1:25:32输出"0";第二名1:26:10与冠军相差 38 秒,输出"+0:38";第三名1:27:05相差 1 分 33 秒,输出"+1:33"。
三、测试用例逐条解析
原题提供了 5 组断言(原挑战文档 的--hints--区段),覆盖了不同规模、不同差距形态的输入,是实现正确性的直接验收标准:
| 输入 | 期望输出 | 覆盖要点 |
|---|---|---|
["1:25:32", "1:26:10", "1:27:05"] | ["0", "+0:38", "+1:33"] | 基础场景:分钟差跨越 1 分钟 |
["1:00:01", "1:00:05", "1:00:10"] | ["0", "+0:04", "+0:09"] | 秒位补零(+0:04而非+0:4) |
["1:10:06", "1:10:23", "1:10:48", "1:12:11"] | ["0", "+0:17", "+0:42", "+2:05"] | 4 人数组、分钟位为 2 位数 |
["0:49:13", ..., "1:02:20"](10 人) | ["0", "+0:02", ..., "+13:07"] | 大规模输入;差值超过 10 分钟;小时为0的前导场景 |
["2:01:15", "2:10:45", ...](8 人) | ["0", "+9:30", ..., "+13:55"] | 分钟差超过 9 分钟,验证+M:SS中 M 可超过个位数 |
第 4 组用例中,冠军为0:49:13,最后一名1:02:20与冠军相差 13 分 7 秒,输出"+13:07"—— 这验证了一个关键点:输出格式中的分钟数可以超过 59(即不需要对分钟做 60 进制回卷),因为差值在分钟维度上直接以十进制呈现。同时第 2 组用例验证了秒数必须保持两位(+0:04而非+0:4)。
四、解题思路与参考实现
4.1 核心思路:统一到秒,再做差
「时间字符串相减」不能直接在字符串层面完成,正确范式是三步走:
- 解析:将
"H:MM:SS"字符串转换为总秒数; - 求差:用每个选手的总秒数减去冠军(数组首项)的总秒数;
- 格式化:把差值秒数转回
"+M:SS"字符串。
该思路与同模块中其他时间类题目(如 Challenge 11「Mile Pace」、Challenge 77「Duration Formatter」、Challenge 148「Tire Pressure」)采用的「统一单位 → 计算 → 格式化」模式一致,可视为每日编程挑战中时间/单位换算题型的通用套路。
4.2 参考实现(原题--solutions--官方解法)
function getRelativeResults(results) { const timeToSeconds = (timeStr) => { const [hours, minutes, seconds] = timeStr.split(':').map(Number); return hours * 3600 + minutes * 60 + seconds; }; const secondsToTimeFormat = (secs) => { const mins = Math.floor(secs / 60); const secsRemainder = secs % 60; return `+${mins}:${String(secsRemainder).padStart(2, '0')}`; }; const winnerSeconds = timeToSeconds(results[0]); return results.map((time, index) => { if (index === 0) return '0'; const currentSeconds = timeToSeconds(time); const difference = currentSeconds - winnerSeconds; return secondsToTimeFormat(difference); }); }4.3 实现细节逐行拆解
解析函数timeToSeconds:timeStr.split(':')将"1:26:10"拆为["1", "26", "10"],map(Number)得到[1, 26, 10],再按小时×3600 + 分钟×60 + 秒求和得5170秒。注意输入允许小时位为 0(如"0:49:13"),该写法天然兼容。
格式化函数secondsToTimeFormat:Math.floor(secs / 60)直接得到分钟数(不取模,因此分钟可超过 59,正确输出"+13:07");secs % 60取余得到秒数;String(...).padStart(2, '0')保证秒数不足两位时补零(+0:04)。这正是模板字符串+${mins}:${...}拼出+M:SS的关键。
主流程:先缓存winnerSeconds(避免在map中重复解析冠军时间);map回调中index === 0直接返回"0";其余元素计算currentSeconds - winnerSeconds后格式化。由于题目保证输入已按快慢排序,差值恒为非负,无需处理负数边界。
4.4 复杂度分析
- 时间复杂度:单次遍历
O(n),其中n为参赛人数;每次解析/格式化均为常数级操作。 - 空间复杂度:仅
map产生一个新数组,O(n)。
该解法无需额外排序或查找,是此类「相对成绩」问题的最优形态之一。
五、边界情况与易错点
- 秒数补零:
"+0:38"中秒数必须两位,直接拼+${mins}:${secs}会得到+0:38正确,但秒为个位数(如 4 秒)时若不补零会输出+0:4导致断言失败——这正是padStart(2, '0')存在的意义。 - 分钟数不进位:差值 97 秒应输出
"+1:37"而非"+1:370"或"+0:97";Math.floor与%的配合保证了分钟/秒的正确分离。 - 冠军本身非零起点:冠军时间可能是
"1:25:32"这类非零时间,不能误以为冠军一定是"0:00:00";必须以results[0]为基准。 - 返回类型:输出数组中每个元素都是字符串(含冠军的
"0"),若返回数字0会在assert.deepEqual严格相等断言中失败。 - 输入不可变性:原解法不修改输入数组(
map返回新数组),符合函数式编程的预期,也与测试框架对纯函数的假设一致。
六、该挑战在 freeCodeCamp 体系中的运行链路
Challenge 184 并非孤立的题目文件,而是贯穿课程、数据管道与 API 全链路的完整功能模块:
1. 课程源文件:挑战本体以 Markdown 形式存放于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/,同一目录下还包含 Challenge 180~196 的 Winter Games 系列文档。每个文件按 freeCodeCamp 标准格式组织--description--、--hints--、--seed--与--solutions--四个区段。
2. 种子数据管道:仓库中的 tools/daily-challenges/helpers.ts 展示了挑战如何被采集:通过 GraphQL 端点按superBlock: "dev-playground"、block: "daily-coding-challenges-javascript"过滤查询挑战节点,将 JavaScript 与 Python 两个版本按challengeNumber与日期合并(combineChallenges)后写入数据库;随后由 tools/daily-challenges/seed-daily-challenges.ts 完成入库,为「每日一题」按日期分发提供数据基础。
3. API 服务端:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 定义了 6 个公开 GET 接口:按日期/daily-coding-challenge/date/:date、按月日/daily-coding-challenge/day/:day、今日/daily-coding-challenge/today、按月份列表/daily-coding-challenge/month/:month、全部/daily-coding-challenge/all、最新日期/daily-coding-challenge/newest。其中日期校验、时区处理等逻辑集中在 api/src/daily-coding-challenge/utils/helpers.ts:getNowUsCentral基于date-fns-tz的getTimezoneOffset('America/Chicago', ...)计算美国中部时区当前时间,getSourceDate负责将任意请求日期映射回 2025-08-11 至 2026-08-10 这一年的源挑战日期(2 月 29 日请求会被映射到 2 月 28 日的挑战),接口对晚于美国中部当日零点的挑战一律返回 404。
4. 响应数据校验:api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts 用 TypeBox 定义了响应结构:单个挑战包含id、date、challengeNumber、title、description及javascript/python两个语言子对象(各自含tests数组与challengeFiles数组);client/src/utils/daily-coding-challenge-validator.ts 则在客户端用 Joi 对同一结构做二次校验,并额外支持可选的disableLoopProtectTests字段。Challenge 184 的tests即由本文第三节列出的 5 组assert.deepEqual断言构成,会被注入浏览器端执行器逐条验证用户提交的函数。
5. 客户端入口:client/src/components/daily-coding-challenge/widget.tsx 在课程地图与首页渲染「每日编码挑战」入口组件,提供「前往今日挑战」与「挑战归档」两个按钮;日历视图组件位于同目录的 calendar.tsx。
七、延伸练习与变式
在完成本题后,可基于同一「解析-计算-格式化」框架尝试以下变式,进一步巩固:
- 输出顺序反转:输入仍按快慢排序,但要求输出按从慢到快的相对差距(差值仍以冠军为基准)。
- 百分比差距:将
"+M:SS"改为落后百分比(如"+1.24%"),考察浮点精度与四舍五入。 - 并列处理:若输入可能出现相同时间(并列名次),设计返回数组如何保持长度一致。
- 健壮性增强:为
timeToSeconds增加非法格式(如缺位、超范围秒数)的防御性校验,参考 API 层dateStringToUtcMidnight用正则预检后再解析的写法。
八、小结
Challenge 184「Cross-Country Skiing」是一道经典的时间差计算题,题目本身结构清晰:"H:MM:SS"解析、秒差计算、"+M:SS"格式化三环节环环相扣。通过逐条核对原题的 5 组断言,并回溯其在 课程源文件、模块配置、API 路由 与 客户端校验器 中的完整链路,可以看到一道看似简单的每日挑战背后,是 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),仅供参考