freeCodeCamp 每日编程挑战解析:Challenge 165 Class Average 成绩平均值与字母等级映射
本篇指南以 freeCodeCamp 课程仓库中的每日编程挑战(Daily Coding Challenges)第 165 题"Class Average"为核心,完整讲解如何基于一组考试分数数组计算平均分,并按照 13 档区间映射表返回 "A+" 到 "F" 的字母等级。读完本文,你既能掌握用 reduce 求均值、用条件链做区间分桶的经典解法,也能通过仓库源码理解这类挑战的题目组织、测试断言与服务端调度机制。
挑战背景:Daily Coding Challenges 中的第 165 题
freeCodeCamp 在课程中维护了一个 daily-coding-challenges-javascript 模块,其中包含数百道面向 JavaScript 学习者的每日一题。Challenge 165 名为 "Class Average",题目位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/694596b0585c11170ac7c7fd.md,其 challengeType 为 28(独立函数实现型练习)。
从模块清单 curriculum/structure/blocks/daily-coding-challenges-javascript.json 可以看到,该模块采用 legacy-challenge-list 布局、归属 JavaScript 帮助分类,并且开启了 usesMultifileEditor。Challenge 165 在该模块中位于 Challenge 161 "Free Shipping" 与 Challenge 166 "Hex Validator" 之间,编号与课程顺序一一对应。这批挑战同时存在 Python 版本,仓库通过 tools/daily-challenges/seed-daily-challenges.ts 以"每年 365 道、逐日递增"的方式写入数据库,供学员按日期作答。
题目要求:从分数数组到字母等级
题目给出一个由考试分数(数字)组成的数组,要求返回这些分数的平均分所对应的字母等级。平均分的计算方式为:将所有分数相加,再除以分数的个数。
等级划分严格遵循以下映射表(原文档完整保留):
| 平均分区间 | 字母等级 |
|---|---|
| 97-100 | "A+" |
| 93-96 | "A" |
| 90-92 | "A-" |
| 87-89 | "B+" |
| 83-86 | "B" |
| 80-82 | "B-" |
| 77-79 | "C+" |
| 73-76 | "C" |
| 70-72 | "C-" |
| 67-69 | "D+" |
| 63-66 | "D" |
| 60-62 | "D-" |
| 低于 60 | "F" |
从表结构可以归纳出两个规律:其一是每个主等级(A/B/C/D)内部再按 3 分一档拆成 +、基准、- 三个子档;其二是区间完全连续、没有缝隙,每个可能的平均分都能且只能命中一个等级。这意味着"从高到低逐档判断、命中即返回"是最自然的实现思路。
起始代码:补全 getAverageGrade
题目提供的种子代码定义了一个名为 getAverageGrade 的函数,接收 scores 数组作为唯一参数:
function getAverageGrade(scores) {
return scores;
}
学习者需要完成两件事:
- 计算
scores中所有数字的平均值; - 依据映射表把平均值转换为对应的字母等级字符串并返回。
值得注意的是种子代码当前 return scores; 只是占位,测试会以各种分数组合调用该函数并严格比对返回值,因此函数名与签名必须保持不变。
官方参考解法逐行解析
题目自带的解决方案使用 reduce 求和,再以从高到低的 if 链逐档判断:
function getAverageGrade(scores) {
const avg =
scores.reduce((sum, score) => sum + score, 0) / scores.length;
if (avg >= 97) return "A+";
if (avg >= 93) return "A";
if (avg >= 90) return "A-";
if (avg >= 87) return "B+";
if (avg >= 83) return "B";
if (avg >= 80) return "B-";
if (avg >= 77) return "C+";
if (avg >= 73) return "C";
if (avg >= 70) return "C-";
if (avg >= 67) return "D+";
if (avg >= 63) return "D";
if (avg >= 60) return "D-";
return "F";
}
这段解法包含三个值得注意的工程细节:
1. 求和使用 reduce 并显式指定初始值 0
scores.reduce((sum, score) => sum + score, 0)
如果不传第二个参数 0,reduce 会把数组首元素当作初始累加值,这在空数组或首元素参与运算方式特殊时会产生差异;显式传入 0 保证了累加语义的确定性。整个挑战系列在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript 目录下大量采用这种函数式写法,是学习 reduce 用法的理想素材。
2. 平均值直接除以 scores.length
题目明确平均分 = 总分 ÷ 分数个数,因此直接使用数组长度即可,无需额外处理。JavaScript 中除法结果可能是小数(例如测试用例 [63, 69, 65, 66, 71, 64, 65] 的平均分为 66.142857…),而 if 链基于 >= 比较,天然兼容非整数的平均值,无需四舍五入——这一点从六个测试用例的预期结果可以验证(详见下一节)。
3. 用区间下限替代上下限双端判断
每个档位只判断下界(如 avg >= 90 即落入 "A-"),因为判断顺序从 97 向下递减,之前未命中意味着 avg < 90 的上界约束已由前序条件隐含保证。这种"单向边界 + 有序判断"的技巧既避免了写 avg >= 90 && avg <= 92 这类冗长条件,也消除了区间缝隙的风险,是区间分桶的标准写法。
测试用例(hints)与断言机制
该题在 YAML 前置元数据之后的 --hints-- 区块定义了 6 个测试用例,仓库中每个用例都同时提供可读描述与 assert 断言代码:
| 输入 scores | 平均分 | 预期返回 | 命中的档位 |
|---|---|---|---|
[92, 91, 90, 94, 89, 93] |
91.5 | "A-" |
90-92 |
[84, 89, 85, 100, 91, 88, 79] |
88 | "B+" |
87-89 |
[63, 69, 65, 66, 71, 64, 65] |
66.14… | "D" |
63-66 |
[97, 98, 99, 100, 96, 97, 98, 99, 100] |
98.22… | "A+" |
97-100 |
[75, 100, 88, 79, 80, 78, 64, 60] |
78 | "C+" |
77-79 |
[45, 48, 50, 52, 100, 54, 56, 58, 59] |
58 | "F" |
低于 60 |
对应的断言代码示例(原文档完整继承):
assert.equal(getAverageGrade([92, 91, 90, 94, 89, 93]), "A-");
assert.equal(getAverageGrade([84, 89, 85, 100, 91, 88, 79]), "B+");
assert.equal(getAverageGrade([63, 69, 65, 66, 71, 64, 65]), "D");
assert.equal(getAverageGrade([97, 98, 99, 100, 96, 97, 98, 99, 100]), "A+");
assert.equal(getAverageGrade([75, 100, 88, 79, 80, 78, 64, 60]), "C+");
assert.equal(getAverageGrade([45, 48, 50, 52, 100, 54, 56, 58, 59]), "F");
六个用例的设计极具针对性:
- 覆盖全部 13 个区间的代表档:A+、A-、B+、C+、D、F 六档分散于上中下三段,验证了条件链在不同高度上的分支;
- 覆盖非整数平均分:第二、三、四组用例的平均分都不是整数,验证了
>=比较对小数平均值的正确性; - 覆盖及格线边界:第六组
[45, 48, 50, 52, 100, 54, 56, 58, 59]平均分为 58,落在 60 分以下,专门验证"F"的兜底分支; - 平均分恰好为整数档位:
[84, 89, 85, 100, 91, 88, 79]平均分为 88,精确落在 87-89 的"B+"档,验证了区间的闭区间语义。
边界情况与进阶优化
在保证通过全部断言的前提下,可以进一步思考以下场景:
- 空数组:
scores.length为 0 时avg为NaN,所有>=比较均为false,最终返回"F"。如需更严谨的语义,可在开头增加if (scores.length === 0) return null;之类的防护,但注意不要破坏题目断言的预期行为。 - 数组未排序:求和与平均值计算与顺序无关,因此解法天然不依赖输入顺序。
- 更简洁的区间表实现:可以改用"下限数组 + 等级数组"的查表法,将 13 个边界抽象为数据而非逻辑:
function getAverageGrade(scores) {
const avg = scores.reduce((sum, score) => sum + score, 0) / scores.length;
const bounds = [97, 93, 90, 87, 83, 80, 77, 73, 70, 67, 63, 60];
const grades = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-"];
for (let i = 0; i < bounds.length; i++) {
if (avg >= bounds[i]) return grades[i];
}
return "F";
}
这种实现把等级规则与判断逻辑解耦,便于日后调整分数线,也是 LeetCode 风格题目中常见的"数据驱动"重构思路。
这套挑战在 freeCodeCamp 中的运行机制
通过仓库源码可以完整还原 Challenge 165 背后的调度与校验链路,这里不展开项目整体介绍,仅围绕每日挑战机制说明与本题目直接相关的三点:
1. 题目以数据形式存库,按日期分发
api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 定义了 GET /daily-coding-challenge/date/:date、/day/:day、/today、/month/:month、/all、/newest 六类公开接口。学员当日拿到的题目通过 Prisma 从 dailyCodingChallenges 集合查询,并且服务端会限制"不返回晚于美国中部时间当天的题目"。
2. 种子脚本按 365 天批量生成
tools/daily-challenges/seed-daily-challenges.ts 从 dev-playground 区块抓取 365 道 JavaScript 与 365 道 Python 题目,校验数量一致后从 2025-08-11 起逐日递增日期写入 MongoDB,bulkWrite 使用 replaceOne + upsert 实现幂等写入。Challenge 165 对应的日期即为起始日后的第 165 天。
3. 响应结构与日期工具均有约束
api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts 规定单个挑战响应必须包含 id、date、challengeNumber、title、description 以及 javascript/python 两套 tests 与 challengeFiles;api/src/daily-coding-challenge/utils/helpers.ts 则处理日期格式校验、美国中部时间换算、闰年 2 月 29 日映射等细节。前端入口 client/src/components/daily-coding-challenge/widget.tsx 则引导用户跳转到当天的挑战页。
小结
Challenge 165 "Class Average" 是一道兼具实用性与教学性的入门级算法题:它要求学习者掌握数组求和(reduce)、均值计算、区间映射三个基础能力,官方解法用 14 行代码演示了"排序条件链 + 单向边界"的经典模式,6 个测试用例则覆盖了非整数均值、整数边界与兜底分支等关键场景。结合 694596b0585c11170ac7c7fd.md 的题目原稿与上述源码佐证,读者既可以把它当作一次完整的 JavaScript 编程练习,也可以借此理解 freeCodeCamp 每日挑战从"题目编写 → 批量入库 → 按日分发 → 自动断言"的完整链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0632
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00