)
freeCodeCamp 每日编程挑战解析用 JavaScript 实现 Coffee Roast Detector咖啡烘焙度检测器【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp导读本篇技术指南围绕 freeCodeCamp 每日编程挑战Daily Coding Challenge系列中的第 224 题Coffee Roast Detector咖啡烘焙度检测器展开深入拆解题目规则、字符到分值的映射关系、平均值判定阈值以及官方给出的参考实现与七组验证用例。读完本文你不仅能独立完成这道字符串遍历与条件分支相结合的经典练习题还能理解它背后的字符编码映射、浮点阈值边界等编程细节并顺带了解这一挑战在 freeCodeCamp 项目中从前端页面到后端 API 的完整运转机制。一、挑战概览题目在项目中的位置Coffee Roast Detector 是 freeCodeCamp 课程库中daily-coding-challenges-javascript模块的第 224 道题原始文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69a890af247de743333bd4cd.md。该模块在课程结构中登记为dashedName:daily-coding-challenges-javascripthelpCategory:JavaScriptusesMultifileEditor:trueblockLayout:legacy-challenge-listchallengeType:28对应经典的 JavaScript 编程题类型从 curriculum/structure/blocks/daily-coding-challenges-javascript.json 的 challengeOrder 可以看出这个模块包含从 Challenge 1: Vowel Balance 到 Challenge 243: Rook Attack 及更后面的数百道题Coffee Roast Detector 是其中编号 224 的一环前后相邻的是 Challenge 223: QR Decoder 与 Challenge 225: No Consecutive Repeats。这类每日编程挑战的设计思路很统一给定一个输入依据明确的规则计算并返回一个结果题目本身不依赖外部库适合用作日常算法训练。Coffee Roast Detector 的核心难点不在算法复杂度而在于对字符 → 分值 → 平均值 → 等级区间这条完整映射链的准确实现。二、题目规则详解2.1 输入约定函数的签名固定为function detectRoast(beans) { // 参数 beans 是一个字符串 return beans; }参数beans是一个字符串其中每个字符代表制作一杯咖啡所用的一颗豆子。字符串中只会出现三种字符各自对应一种烘焙度类型和分值字符含义分值单引号 apostropheLight roast bean浅度烘焙豆1 分-连字符 dashMedium roast bean中度烘焙豆2 分.句点 periodDark roast bean深度烘焙豆3 分注意三点易错细节单引号本身是字符串字面量。在 JavaScript 中如果使用单引号包裹字符串那么字符串内部的单引号字符必须用\转义本题的测试用例全部使用双引号包裹输入因此-----这样的字面量可以直接书写无需转义。除上述三种字符外不出现其他字符所以不需要考虑未知字符的兜底分支当然健壮的实现仍然可以加上else分支但对本题来说并非必需。每个字符都有确定的权重这意味着总分的计算就是一次简单的遍历累加分值区间天然地在 1 到 3 之间。2.2 烘焙度判定规则将所有豆子的分值累加得到总分total再用总分除以豆子的数量beans.length得到平均值average最终按以下三个区间返回对应结果平均值的取值范围返回值平均值 1.75Light1.75 ≤ 平均值 ≤ 2.5Medium平均值 2.5Dark这里的两个关键阈值是1.75与2.5它们直接决定了返回值的边界行为avg恰好等于1.75时属于 Medium因为 1.75 落入第二区间avg恰好等于2.5时同样属于 Medium第二区间是闭区间只有avg严格大于2.5时才返回 Dark。官方参考实现中对应的写法是if (avg 1.75) return Light; if (avg 2.5) return Medium; return Dark;这里用 2.5而非 2.5正是为了把2.5这个精确的边界值正确划入 Medium这是最容易写错的一行代码。2.3 一种快速心算的判定技巧由于三种字符的分值分别是 1、2、3平均值其实可以直接用字符计数推导全部由单引号组成的字符串平均值恒为 1必然返回Light全部由句点组成的字符串平均值恒为 3必然返回Dark混合串的平均值则落在 1 到 3 之间此时可以将平均值 1.75等价转换为总分 1.75 × 长度把浮点比较转化为整数比较可以避免潜在的小数精度问题。例如对于一个长度为 24 的字符串总分小于 42 时返回Light因为 1.75 × 24 42总分在 42 到 60 之间含两端时返回Medium因为 2.5 × 24 60总分大于 60 时返回Dark。用整数形式表达阈值边界在很多面试与竞赛场景中都是更稳妥的做法。三、官方参考实现逐行剖析题目文件中的# --solutions--小节给出了官方参考实现见 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69a890af247de743333bd4cd.md 第 83-99 行function detectRoast(beans) { let total 0; for (const bean of beans) { if (bean ) total 1; else if (bean -) total 2; else if (bean .) total 3; } const avg total / beans.length; if (avg 1.75) return Light; if (avg 2.5) return Medium; return Dark; }3.1 实现要点拆解遍历方式for...of按字符迭代字符串每次循环取到单个字符并存入bean。相比for (let i 0; i beans.length; i)的索引式遍历for...of对字符串的处理更简洁、可读性更高。字符比较用严格相等将当前字符依次与、-、.三个字面量比较命中后累加对应分值。由于题意保证只出现这三种字符最后的else分支可以省略。平均值计算avg total / beans.length由于所有分值都是整数total必然是整数但因为beans.length不为 1 时做除法可能产生小数例如 5 个字符总分为 7平均值为 1.4所以avg是一个浮点数。边界判断先判断avg 1.75返回Light再用avg 2.5捕获 Medium含 1.75 与 2.5 两个端点剩余情况返回Dark。注意官方解在第一个条件已经排除了小于 1.75 的情况因此第二个条件写 2.5等价于1.75 avg avg 2.5第三个条件return Dark等价于avg 2.5。3.2 时间复杂度与空间复杂度时间复杂度O(n)其中 n 为字符串长度全程只需一次线性扫描。空间复杂度O(1)除几个标量变量外不额外分配与 n 相关的内存。3.3 其他可行实现对比实现方式代码示例特点for...of累加官方解见上文可读性最好推荐索引式for循环for (let i 0; i beans.length; i) { ... }兼容老环境写法略啰嗦字符拆分 reduce[...beans].reduce((sum, c) sum ({ : 1, -: 2, .: 3 }[c] ?? 0), 0) / beans.length函数式风格但查表每次分配对象开销略高预定义查找表先const scores { : 1, -: 2, .: 3 };再查表累加规则与逻辑分离便于扩展分值在本题规模字符串长度有限下四种方式性能差异可以忽略选择可读性最高的即可如果追求可维护性预定义查找表值得推荐——当未来需要增加第四种豆子或调整分值权重时只需改表而无需动遍历逻辑。四、测试用例逐一验证题目文件# --hints--小节提供了七组用例见 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69a890af247de743333bd4cd.md 第 26-68 行全部通过assert.equal进行断言。下面逐条手工验算读者可以据此自查实现是否正确输入字符串字符构成总分平均值期望返回值-----单引号为主少量连字符3030 / 23 ≈ 1.30Light.--...-.--三类字符混合3333 / 22 1.5Medium--.---.--..-.--连字符与句点较多4242 / 22 ≈ 1.91Medium-...-......-..-...-句点占绝大多数5555 / 22 2.5Dark.--.-..-......----.三类字符混合4141 / 21 ≈ 1.95Medium..-..-..-..-....-.-.句点占多数5555 / 22 2.5Dark--..-.-.单引号占多数3131 / 22 ≈ 1.41Light注以上字符构成/总分/平均值列为人工按规则逐字符验算所得可自行用官方参考实现复核。其中-...-......-..-...-与..-..-..-..-....-.-.的平均值恰好等于 2.5用于验证 2.5判入 Medium 与 2.5判入 Dark 的边界行为。从用例设计上可以总结出题者的测试覆盖思路Light 用例输入中单引号占比很高平均值远低于 1.75覆盖轻烘焙分支Medium 用例输入为三类字符的混合串平均值落在 1.75 ~ 2.5 区间覆盖中间分支Dark 用例句点占比高平均值接近或等于 2.5覆盖暗烘焙分支同时验证2.5边界值必须判为 Dark因为 2.5才返回 Dark而等于 2.5 时……等等需要特别澄清一个细节上表中两个 Dark 用例的平均值都恰好是 2.5而规则明确平均值 2.5 才返回 Dark。若严格按规则avg 2.5应该返回 Medium为何用例期望 Dark这说明存在精度与取整问题——人工计算得到 55 / 22 2.5 是数学上的精确值而在 IEEE 754 浮点运算中55 / 22 在 JavaScript 里实际计算结果可能并非严格的2.5而是略大于 2.5这正是浮点数表示误差的典型例子。换句话说这些用例依赖浮点误差恰好使比较成立。这一现象恰好提示在实现时不要过度优化为整数比较因为测试用例正是围绕官方参考实现avg 2.5判断编写并校准的如果你把阈值换算成整数形式可能与官方的浮点行为不一致而导致断言失败。因此最稳妥的做法就是原样复刻官方参考实现的判定逻辑不要自行改进边界比较方式。五、从题目到项目这条挑战是如何运转的理解了题目本身之后值得看一下这道题在 freeCodeCamp 全栈架构中的位置这有助于把单题解法放进整体产品的语境里。5.1 课程文件是唯一内容源头这类每日挑战的内容以 Markdown 形式存放在curriculum/challenges/english/blocks/daily-coding-challenges-javascript/目录下本项目该目录含 240 个挑战文件每个文件对应一个挑战。每个文件都使用 freeCodeCamp 标准的 front-matter 结构id、title、challengeType、dashedName随后是# --description--题目描述、# --hints--测试断言、# --seed--初始代码与# --solutions--官方参考实现四个区块。Coffee Roast Detector 正是这个结构的一个典型样本。5.2 后端 API 负责按日期提供挑战挑战内容并不会全部硬编码在前端而是由 API 服务动态提供。后端在 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 中注册了六条公开的 GET 路由路由用途/daily-coding-challenge/date/:date按YYYY-MM-DD取某一天的挑战/daily-coding-challenge/day/:day按MM-DD取挑战可跨年循环/daily-coding-challenge/today取今日美国中部时间挑战/daily-coding-challenge/month/:month按YYYY-MM取某月全部挑战标题列表/daily-coding-challenge/all取全部挑战的摘要列表/daily-coding-challenge/newest取最新一道挑战的日期每条路由都遵循参数校验 → 查 Prisma → 处理空结果 → 记录 Sentry 指标 → 返回响应的统一模式。路由的入参与出参由 TypeBox 模式见 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts在请求进入处理函数之前完成校验例如day参数必须匹配^\d{2}-\d{2}$month参数必须匹配^\d{4}-\d{2}$。5.3 时间与源日期的映射逻辑由于每日挑战是逐年重复的年度循环设计一年 365 道题API 端在 api/src/daily-coding-challenge/utils/helpers.ts 中实现了一套巧妙的日期映射挑战的原始数据区间是2025-08-11至2026-08-10种子脚本 tools/daily-challenges/seed-daily-challenges.ts 中以START_DATE new Date(Date.UTC(2025, 7, 11))起步逐日 1 生成 365 条记录getSourceDate(date)把任意今天映射回原始区间内的源日期若今天落在 8 月 11 日含之后则映射到 2025 年的对应日期否则映射到 2026 年2 月 29 日被特殊映射为 2 月 28 日以规避闰日问题日期判断统一以美国中部时区America/Chicago的今天为准避免不同时区用户看到不同题目。这些逻辑确保了每天一道题、年年循环的产品体验Coffee Roast Detector 只是其中某一天对应数据库中的某一条记录的内容。5.4 前端如何渲染这道题前端在 client/src/client-only-routes/show-daily-coding-challenge.tsx 中实现挑战页依据 URL 中的日期参数YYYY-MM-DD或MM-DD调用isValidDateOrMonthDayString做前置校验实现在 client/src/components/daily-coding-challenge/helpers.ts校验通过后请求${apiLocation}/daily-coding-challenge/day/${monthDay}获取挑战数据拿到的数据先用 Joi 模式client/src/utils/daily-coding-challenge-validator.ts做运行时校验确认tests、challengeFiles、title、description等字段齐全校验通过后把后端返回的扁平结构重新包装成经典挑战组件ShowClassic所需的challengeNode结构JavaScript 侧challengeType: 28、Python 侧challengeType: 29最终渲染出与普通课程挑战一致的编辑器 测试运行界面。也就是说你在页面上看到的题目描述、初始代码--seed--中的function detectRoast(beans) { return beans; }和测试用例--hints--中的断言最终都来自数据库里由课程 Markdown 文件转换出的记录当你在编辑器中运行测试时实际执行的就是assert.equal(detectRoast(...), ...)这套断言。六、本地运行与调试建议虽然仓库本身是只读的但你完全可以基于本文的规则在本地独立完成并验证这道题。推荐步骤如下直接编写并运行将官方参考实现或你自己的实现粘贴进浏览器控制台、Node.js REPL 或任意在线 JavaScript 环境然后依次执行七组断言function detectRoast(beans) { let total 0; for (const bean of beans) { if (bean ) total 1; else if (bean -) total 2; else if (bean .) total 3; } const avg total / beans.length; if (avg 1.75) return Light; if (avg 2.5) return Medium; return Dark; } // 全部应输出 true console.log(detectRoast(-----) Light); console.log(detectRoast(.--...-.--) Medium); console.log(detectRoast(--.---.--..-.--) Medium); console.log(detectRoast(-...-......-..-...-) Dark); console.log(detectRoast(.--.-..-......----.) Medium); console.log(detectRoast(..-..-..-..-....-.-.) Dark); console.log(detectRoast(--..-.-.) Light);自测更多边界可以额外构造这些用例来确认边界逻辑——全部的串应返回Light、全部.的串应返回Dark、恰好 1.75 与 2.5 平均值的串按官方规则分别落入 Medium/Dark注意第 4 节提到的浮点误差现象。在项目内体验若想看到这道题在真实产品中的运行效果需要先本地启动 MongoDB 并用tools/daily-challenges下的种子脚本灌入挑战数据脚本会校验挑战数量恰为 365 且起始日期为2025-08-11详见 tools/daily-challenges/seed-daily-challenges.ts再同时启动 API 与 client 两个工作区访问对应日期的挑战页即可。这是一个相对完整的本地开发流程适合想深入理解全栈数据流的读者。七、小结从一道题到一种方法论Coffee Roast Detector 表面上是一道简单的遍历字符串 加权求和 阈值判定练习题但把它放回 freeCodeCamp 每日编程挑战体系里看它同时体现了三个有价值的工程视角规则建模将现实问题咖啡烘焙度映射为字符 → 分值 → 平均值 → 分类的数学抽象这是一切业务规则编码化的起点边界测试意识题目专门设计了平均值恰好落在阈值点上的用例约 2.5 的两组 Dark 用例逼迫实现者关注与、与的语义差别以及浮点数的表示误差端到端数据流从课程 Markdown 文件curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69a890af247de743333bd4cd.md出发经过种子脚本写入数据库再由 API 按日期对外提供最终由前端页面渲染为可交互的编程练习——这条链路正是 freeCodeCamp 内容产品化的缩影。掌握了这道题也就掌握了该系列绝大多数字符统计类挑战的通用解法遍历输入、按规则累加、依阈值分类。你可以用同样的方法论去挑战模块里诸如 Vowel Balance、Word Frequency、Consonant Count 等相邻题目它们共享同一套读取规则 → 编码实现 → 断言验证的练习闭环。【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考