freeCodeCamp 每日编程挑战 360:Spoken Duration——把秒数翻译成口语化英语时长
【免费下载链接】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)系列中的 Challenge 360 "Spoken Duration" 为核心,完整讲解这道题的原始需求、全部测试断言与参考解法:如何把一个纯数字的秒数,按小时/分钟/秒拆解后组合成符合英语口语习惯的时长表达(如"1 hour, 2 minutes and 3 seconds"),并结合同仓库中挑战类型定义与 block 配置文件,说明这道题在 freeCodeCamp 课程体系中的实际落地机制。读完后你可以独立完成这类"数值拆解 + 单复数 + 英文连接词"的字符串组装问题,并理解 freeCodeCamp 挑战文件的 frontmatter、hints、seed 与 solutions 各段落的结构约定。
题目背景:每日编程挑战中的 Challenge 360
该题定义在 6a26df3e2988bcdded204896.md,属于daily-coding-challenges-javascript板块(JavaScript 版每日挑战)。在 block 结构文件 daily-coding-challenges-javascript.json 中可以看到它的登记条目:
{ "id": "6a26df3e2988bcdded204896", "title": "Challenge 360: Spoken Duration" }它位于 Challenge 358 "Emoji Translator" 与 Challenge 361 "Spoken Time" 之间(后者是它的"姊妹题",处理的是带 AM/PM 的钟表时间)。该 block 的顶部配置还声明了若干运行特性,直接影响做题体验:
{ "isUpcomingChange": true, "dashedName": "daily-coding-challenges-javascript", "usesMultifileEditor": true, "helpCategory": "JavaScript", "disableLoopProtectTests": true, "blockLayout": "legacy-challenge-list", ... }其中disableLoopProtectTests: true可以理解为关闭了防死循环保护的测试包装,意味着解法中如果出现无限循环不会被特殊熔断逻辑提前截断;usesMultifileEditor: true表示该系列题使用多文件编辑器界面。
挑战类型 28 意味着什么
题目 frontmatter 中的challengeType: 28是理解运行方式的关键。在 challenge-types.ts 中,类型常量的定义确认了 28 即dailyChallengeJs:
const dailyChallengeJs = 28; const dailyChallengePy = 29;同文件还给出了这类挑战的两条行为映射:
// determine the component view for each challenge export const viewTypes = { ... [dailyChallengeJs]: 'classic', ... }; // determine the type of submit function to use on challenge completion export const submitTypes = { ... [dailyChallengeJs]: 'tests', ... };也就是说,这道题在前端以classic(经典代码编辑器)视图呈现,提交时走tests提交流程——把用户代码与内置断言一起执行。这也解释了为什么题目文件中的每条提示都是一个assert.equal(...)断言:它们就是判题依据本身。该文件还提供了getIsDailyCodingChallenge、getDailyCodingChallengeLanguage等辅助函数,用于在站点层面区分每日挑战并映射语言(dailyChallengeJs→'javascript')。
题目要求(完整继承)
原题给出的完整需求如下:
Given a number of seconds, return the duration in spoken English.
- Break the duration into hours, minutes, and seconds.
- Skip any zero values.
- Use singular or plural as appropriate (
"1 hour","2 hours").- If present, join the last two units with
"and", and the second and third to last units with a comma ("1 hour, 2 minutes and 3 seconds").
逐条翻译为可验证的约束:
- 拆解:输入秒数必须拆成小时、分钟、秒三个单位(1 小时 = 3600 秒,1 分钟 = 60 秒)。
- 跳过零值:某个单位的计数为 0 时不输出该单位,而不是输出
"0 minutes"之类。 - 单复数:计数为 1 时用单数(
hour/minute/second),否则用复数(hours/minutes/seconds)。 - 英文连接词:当存在多个单位时,最后两个单位之间用
" and "连接;倒数第三与倒数第二单位之间用逗号(", ")分隔。即三单位时形如"A hours, B minutes and C seconds",两单位时形如"A hours and B seconds"。
官方断言用例
题目--hints--段落给出了 7 条官方断言,覆盖了全部关键分支(含三单位、两单位、单单位、跨小时跳过分钟、单数形式):
| 输入 | 期望输出 | 考察点 |
|---|---|---|
3723 | "1 hour, 2 minutes and 3 seconds" | 三单位全齐 + 小时取单数 |
7295 | "2 hours, 1 minute and 35 seconds" | 三单位 + 分钟取单数 |
8521 | "2 hours, 22 minutes and 1 second" | 三单位 + 秒取单数 |
435 | "7 minutes and 15 seconds" | 小时为 0 被跳过 |
14455 | "4 hours and 55 seconds" | 分钟为 0 被跳过 |
72000 | "20 hours" | 仅剩单单位,无连接词 |
1 | "1 second" | 最小输入 + 单数 |
以文档原样保留的断言形式:
assert.equal(getSpokenDuration(3723), "1 hour, 2 minutes and 3 seconds"); assert.equal(getSpokenDuration(7295), "2 hours, 1 minute and 35 seconds"); assert.equal(getSpokenDuration(8521), "2 hours, 22 minutes and 1 second"); assert.equal(getSpokenDuration(435), "7 minutes and 15 seconds"); assert.equal(getSpokenDuration(14455), "4 hours and 55 seconds"); assert.equal(getSpokenDuration(72000), "20 hours"); assert.equal(getSpokenDuration(1), "1 second");初始代码(seed)
做题时编辑器中给出的骨架(--seed-contents--段落):
function getSpokenDuration(seconds) { return seconds; }初始实现只是原样返回入参,需要完全改写。注意入参名为seconds,与输出单位seconds同名,改写时用局部变量名(如secs)避免混淆。
参考解法逐行解析
--solutions--段落给出的官方参考解:
function getSpokenDuration(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; const parts = []; if (hours) parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); if (minutes) parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); if (secs) parts.push(`${secs} ${secs === 1 ? "second" : "seconds"}`); if (parts.length === 1) return parts[0]; if (parts.length === 2) return `${parts[0]} and ${parts[1]}`; return `${parts[0]}, ${parts[1]} and ${parts[2]}`; }可以拆成三个阶段理解:
阶段一:借位拆解。
hours = Math.floor(seconds / 3600):整除得到小时数;minutes = Math.floor((seconds % 3600) / 60):先取余去掉整小时,再整除 60 得到分钟数——注意必须用seconds % 3600而不是seconds / 3600 % 60,前者直接隔离出"小时内"的秒数;secs = seconds % 60:最终余数即零头秒。
三个变量相加恒等于输入秒数(hours * 3600 + minutes * 60 + secs === seconds),这是后续手工验证用例的基础。
阶段二:按需收集非零片段。parts数组只推入非零单位,每条推入的字符串内联完成了单复数判断(hours === 1 ? "hour" : "hours"三处同理)。if (hours)/if (minutes)/if (secs)的 truthy 判断天然实现了"跳过零值"——0 是 falsy,对应单位不会进入parts。
阶段三:按长度拼接连接词。拼接策略完全由parts.length决定,与题目第 4 条约束一一对应:
length === 1:直接返回唯一片段,没有任何连接词(对应"20 hours"、"1 second");length === 2:A and B(对应"4 hours and 55 seconds"、"7 minutes and 15 seconds");length === 3:A, B and C(对应前三个三单位用例)。
用例手工验证
用阶段一的恒等式逐条核对官方断言:
| 输入 | hours / minutes / secs | parts 内容 | 拼接分支 | 结果 |
|---|---|---|---|---|
3723 | 1 / 2 / 3 | ["1 hour", "2 minutes", "3 seconds"] | 3 段 | "1 hour, 2 minutes and 3 seconds"✓ |
7295 | 2 / 1 / 35 | ["2 hours", "1 minute", "35 seconds"] | 3 段 | "2 hours, 1 minute and 35 seconds"✓ |
8521 | 2 / 22 / 1 | ["2 hours", "22 minutes", "1 second"] | 3 段 | "2 hours, 22 minutes and 1 second"✓ |
435 | 0 / 7 / 15 | ["7 minutes", "15 seconds"] | 2 段 | "7 minutes and 15 seconds"✓ |
14455 | 4 / 0 / 55 | ["4 hours", "55 seconds"] | 2 段 | "4 hours and 55 seconds"✓ |
72000 | 20 / 0 / 0 | ["20 hours"] | 1 段 | "20 hours"✓ |
1 | 0 / 0 / 1 | ["1 second"] | 1 段 | "1 second"✓ |
7 条官方断言全部命中。从源码结构看,解法没有对输入0做防御(此时parts为空、parts.length走最后的 3 段分支会拼出undefined),但官方断言集未覆盖该输入,做题时无需处理;若在自己的项目中复用此逻辑,建议显式约定0的返回值。
可迁移的写法要点
- "先收集、后拼接"是处理不定长枚举输出的稳妥模式:把每个候选单位作为独立片段推入数组,最后只按数组长度选择连接模板,避免了在拆解阶段就预测"这是几个单位"的分支混乱;
- truthy 过滤 + 模板字符串内联三元让"跳过零值"与"单复数"两条规则在同一个表达式里完成,无需额外状态;
- 单复数判断必须发生在片段构造时,因为拼接阶段拿到的已是成品字符串,无法再追溯数量。
这道题在 freeCodeCamp 课程系统中的位置
结合仓库内的实现证据,可以确认该题的完整落地链路:
- 内容层:题目正文以 freeCodeCamp 的 challenge Markdown 格式书写,
--description--(题目)、--hints--(断言提示)、--seed--/--seed-contents--(初始代码)、--solutions--(参考解)各段落语义固定;frontmatter 中的id、title、dashedName(challenge-360)用于站点路由与锚点。 - 结构层:daily-coding-challenges-javascript.json 的
challengeOrder数组按 id 注册题目顺序,决定 360 在挑战列表中的位置。 - 运行层:
challengeType: 28经 challenge-types.ts 映射为classic视图与tests提交类型,用户提交后代码与--hints--中的断言一同执行判定。
该系列中编号相邻的 Challenge 361: Spoken Time 处理的是 12 小时制钟面时间,与本題的"时长口语化"形成对照,适合作为延伸练习。
小结
Challenge 360 用一个短小的函数题集中考察了四件事:整除取余的时间拆解、零值过滤、英文单复数分支、以及不定长元素的连接词拼装。官方参考解的核心模式——拆解为hours/minutes/secs→ truthy 过滤进parts数组 → 按parts.length三档拼接——可以直接迁移到任何"把结构化数值渲染成口语/文案"的场景。题目原文、全部断言与参考解均可在 6a26df3e2988bcdded204896.md 中核对。
【免费下载链接】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),仅供参考