freeCodeCamp 每日编程挑战解析:Jet Lagged 时差计算(Challenge 302,JavaScript 版)
【免费下载链接】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 Challenge)块的第 302 道题「Jet Lagged」展开:给定出发城市、到达城市、飞行时长与航行方向,编写getJetLagHours函数计算旅行者的时差小时数。文章完整复刻题目规格、公式与全部 7 个测试用例,逐行解读官方参考实现,并结合当前仓库中块配置、双语挑战文件、API 拉取路由与客户端校验器等源码,说明这道挑战在 freeCodeCamp 体系中从编写到运行的完整机制。读完你可以独立完成该题,并掌握其背后的"查表 + 线性公式 + 一位小数舍入"编程范式。
一、挑战背景:daily-coding-challenges-javascript 块
这道题属于 freeCodeCamp 课程结构中的daily-coding-challenges-javascript块,所有挑战文件集中存放在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript 目录下,每题一个 Markdown 文件。块的元数据定义在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中,从该配置文件可以确认以下关键事实:
challengeOrder按顺序登记了从 Challenge 1: Vowel Balance 到 Challenge 365: The Last Challenge: Bucket Fill 3 共365 道每日挑战,本题目为其中的第 302 道;usesMultifileEditor: true,即该块内的挑战在多文件编辑器中运行;helpCategory: "JavaScript",帮助分类归入 JavaScript;disableLoopProtectTests: true,禁用了防死循环保护测试,便于算法类解答自由编写循环;isUpcomingChange: true,标识为新版课程体系中即将启用(upcoming)的内容;blockLayout: "legacy-challenge-list",块内挑战以列表布局展示。
同时,daily-coding-challenges-javascript与daily-coding-challenges-python两个块被登记进 curriculum/structure/superblocks/dev-playground.json 的块列表中,说明这套每日挑战以 JavaScript / Python 双语形式提供。本题目就存在两个语言版本:本文分析的 JavaScript 版(challengeType: 28)与 Python 版(challengeType: 29,见 curriculum/challenges/english/blocks/daily-coding-challenges-python/6a0dcd03ee4e68698080ef66.md)。
二、题目陈述与数据规格
题目原文要求:给定一个出发城市(departure city)、一个到达城市(arrival city)、飞行时长(小时)以及航行方向(travel direction),返回旅行者正在经历的时差小时数(jet lag hours)。函数签名为:
function getJetLagHours(departureCity, arrivalCity, flightDuration, direction) { return departureCity; }四个参数的含义如下:
| 参数 | 类型 | 说明 | | - | - | - | |departureCity| string | 出发城市名,来自下表给出的 7 个城市 | |arrivalCity| string | 到达城市名,同样来自下表 | |flightDuration| number | 飞行时长,单位为小时 | |direction| string | 航行方向,取值为"east"或"west"|
题目给出的城市名单及其 UTC 偏移量(相对于 UTC 的小时数)是唯一的合法数据源,必须完整继承:
| City | Offset | | - | - | |"Los Angeles"| -8 | |"New York"| -5 | |"London"| 0 | |"Istanbul"| +3 | |"Dubai"| +4 | |"Hong Kong"| +8 | |"Tokyo"| +9 |
注意"London"的偏移为 0,即伦敦为 UTC 基准城市;"Los Angeles"与"New York"为负偏移(UTC 以西),其余城市为正偏移(UTC 以东)。
三、计算公式拆解
题目给出的时差计算分三步进行:
- 求两城时差:计算两个城市之间的时区差(小时),即
arrivalCity的 UTC 偏移与departureCity的 UTC 偏移之差的绝对值; - 确定方向乘子(direction multiplier):若航行方向为
"east",乘子为1.5;否则(即"west")为1.0; - 套用公式求时差小时数:
jetLagHours = timezoneDifference + (flightDuration * 0.1) * directionMultiplier最终结果需四舍五入到一位小数后返回。
公式的业务含义可以这样理解:时差小时数由两部分构成——两城间的固定时区差(反映生物钟被"直接搬移"的跨度),以及飞行时长折算的疲劳加成(每 10 小时飞行折算 1 小时,向东飞行因昼夜节律提前而放大 1.5 倍,向西飞行保持 1.0 倍)。这道题的编程考点在于:城市到偏移量的查表映射、Math.abs求绝对差值、按方向条件选择乘子,以及最终的一位小数舍入。
四、测试用例全览与手算验证
原文档共给出 7 个测试用例,全部使用assert.equal做严格相等断言,测试的是四舍五入后精确到一位小数的数值。逐一列出并手算验证如下:
| # | 调用 | 期望结果 | 计算过程 | | - | - | - | - | | 1 |getJetLagHours("Istanbul", "Hong Kong", 10, "east")|6.5| diff = |8−3| = 5;5 + (10×0.1)×1.5 = 5 + 1.5 =6.5| | 2 |getJetLagHours("London", "New York", 8, "west")|5.8| diff = |−5−0| = 5;5 + (8×0.1)×1.0 = 5 + 0.8 =5.8| | 3 |getJetLagHours("Hong Kong", "Tokyo", 4, "east")|1.6| diff = |9−8| = 1;1 + (4×0.1)×1.5 = 1 + 0.6 =1.6| | 4 |getJetLagHours("Dubai", "London", 7, "west")|4.7| diff = |0−4| = 4;4 + (7×0.1)×1.0 = 4 + 0.7 =4.7| | 5 |getJetLagHours("Los Angeles", "Hong Kong", 15, "west")|17.5| diff = |8−(−8)| = 16;16 + (15×0.1)×1.0 = 16 + 1.5 =17.5| | 6 |getJetLagHours("Tokyo", "Dubai", 9, "west")|5.9| diff = |4−9| = 5;5 + (9×0.1)×1.0 = 5 + 0.9 =5.9| | 7 |getJetLagHours("New York", "Istanbul", 10, "east")|9.5| diff = |3−(−5)| = 8;8 + (10×0.1)×1.5 = 8 + 1.5 =9.5|
用例设计有意识地覆盖了各种情形:跨 UTC 线的城市对(Los Angeles → Hong Kong,时差达 16 小时)、包含 0 偏移的伦敦(London → New York)、同一半球的短程航线(Hong Kong → Tokyo,时差仅 1 小时)、负偏移出发城市(New York → Istanbul)以及"东/西"两种方向。任何实现若漏掉绝对值、乘子或舍入任一环节,都会被其中某个用例精确捕获。
五、JavaScript 官方参考实现逐行解读
原文档的--solutions--段给出了完整官方解答,这里是逐行展开的版本,附带每步说明:
function getJetLagHours(departureCity, arrivalCity, flightDuration, direction) { // 1. 城市名 -> UTC 偏移量的查表映射 const offsets = { "Los Angeles": -8, "New York": -5, "London": 0, "Istanbul": +3, "Dubai": +4, "Hong Kong": +8, "Tokyo": +9 }; // 2. 通过键查找出发/到达城市的偏移量 const depOffset = offsets[departureCity]; const arrOffset = offsets[arrivalCity]; // 3. 时区差取绝对值(保证结果非负) const timezoneDiff = Math.abs(arrOffset - depOffset); // 4. 方向乘子:east -> 1.5,其余(west)-> 1.0 const multiplier = direction === "east" ? 1.5 : 1.0; // 5. 套用公式 const score = timezoneDiff + (flightDuration * 0.1) * multiplier; // 6. 四舍五入到一位小数:先乘 10、round 后再除回 return Math.round(score * 10) / 10; }几个实现细节值得展开:
- 查表映射:用对象字面量
offsets建立城市名到偏移量的 O(1) 查找表,比switch或一串if/else更简洁、更易扩展; - 绝对时差:
Math.abs(arrOffset - depOffset)消去了"从西到东"与"从东到西"的符号问题,因此函数对任意城市顺序都返回非负时差; - 三元表达式选乘子:
direction === "east" ? 1.5 : 1.0精确对应题目"if travelling east, it's 1.5, otherwise, it's 1.0"的表述,任何非"east"值(包括"west")都落入 1.0 分支; - 一位小数舍入:
Math.round(score * 10) / 10是 JavaScript 中"四舍五入到 n 位小数"的经典手写方式。注意Number.prototype.toFixed(1)返回的是字符串而非数字,且存在二进制浮点舍入行为差异(如(1.005).toFixed(2)的经典陷阱),因此assert.equal严格数值断言下使用Math.round(score * 10) / 10更可靠。
六、Python 版本对照:同一道题的另一种表达
同 ID 的 Python 版位于 curriculum/challenges/english/blocks/daily-coding-challenges-python/6a0dcd03ee4e68698080ef66.md,函数名为get_jet_lag_hours,核心算法完全一致,仅语法与测试写法不同:
def get_jet_lag_hours(departure_city, arrival_city, flight_duration, direction): offsets = { "Los Angeles": -8, "New York": -5, "London": 0, "Istanbul": 3, "Dubai": 4, "Hong Kong": 8, "Tokyo": 9 } timezone_diff = abs(offsets[arrival_city] - offsets[departure_city]) multiplier = 1.5 if direction == "east" else 1.0 score = timezone_diff + (flight_duration * 0.1) * multiplier return round(score, 1)两版差异点归纳:
- 命名规范:JS 用 camelCase(
getJetLagHours),Python 用 snake_case(get_jet_lag_hours); - 舍入 API:Python 直接用内置
round(score, 1)即可得到一位小数的浮点数,JS 则需Math.round(score * 10) / 10手工实现; - 测试注入方式:JS 版测试直接写
assert.equal(getJetLagHours(...), 6.5);Python 版则通过runPython在浏览器中执行 Python 测试代码,借助标准库unittest.TestCase().assertEqual(...)完成断言,例如:
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(get_jet_lag_hours("Istanbul", "Hong Kong", 10, "east"), 6.5)`) }})- 种子代码:JS 版种子为
return departureCity;(占位返回),Python 版种子为return departure_city,均只要求学习者填充函数体。
七、从源码看这道挑战的完整运作机制
挑战文件本身只是课程内容的一部分,freeCodeCamp 通过一整套链路把6a0dcd03ee4e68698080ef66.md这类文件变成用户可在线解答、可自动判题的每日挑战。
1. 文件结构约定。每个挑战 Markdown 以 YAML frontmatter 开头,声明id(全局唯一)、title、challengeType(28 对应 JavaScript 版每日挑战、29 对应 Python 版)与dashedName;正文通过# --description--(题目描述)、# --hints--(断言测试)、# --seed--/--seed-contents--(种子代码)与# --solutions--(官方解答)等分区标记组织,课程构建器据此解析并渲染。
2. 块与超级块配置。块的挑战顺序与编辑器配置定义在 curriculum/structure/blocks/daily-coding-challenges-javascript.json,两个双语块被收录进 curriculum/structure/superblocks/dev-playground.json。
3. API 按日期下发挑战。后端在 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 中注册了公开 GET 路由/daily-coding-challenge/date/:date:先解析YYYY-MM-DD格式日期(格式非法返回 400),再通过 Prisma 查询dailyCodingChallenges表,若日期晚于美国中部时区的今天(challenge.date > getUtcMidnight(getNowUsCentral()))或记录不存在则返回 404,命中则返回挑战完整数据,并同步向 Sentry 记录dcc.challenge_viewed等指标。文件注释还说明:该接口只用于获取挑战信息,挑战提交仍走主挑战完成路由。
4. 客户端数据校验。前端通过 client/src/utils/daily-coding-challenge-validator.ts 用 Joi 校验从数据库下发的挑战结构:要求顶层包含id、challengeNumber(≥1 的整数)、title、date、description,且javascript与python两个语言版本都必须携带非空的tests(含text与testString)与challengeFiles(含fileKey与contents)数组——这正是本挑战文件中--hints--与--seed-contents--经解析后在数据层的对应形态。
5. 前端展示组件。用户界面上,client/src/components/daily-coding-challenge 目录提供了日历(calendar.tsx)、小部件(widget.tsx)、未找到提示(not-found.tsx)等组件;其中 helpers.ts 基于date-fns与date-fns-tz实现"今天"的判定——按America/Chicago时区格式化日期、校验yyyy-MM-dd与MM-DD格式、并把 2 月 29 日映射到 2 月 28 日(toMonthDay),与后端"挑战日期截止于美国中部今天"的逻辑保持一致。
八、延伸思考与边界情况
官方实现已能通过全部测试,但值得留意的边界情况有:
- 未知城市:若传入表中不存在的城市名,
offsets[departureCity]返回undefined,arrOffset - depOffset会得到NaN,assert.equal无法通过。题目保证输入合法,因此官方解答未做兜底;如需健壮实现,可先判断departureCity in offsets再计算。 - 舍入方向:
Math.round遵循"四舍五入到最近的整数,.5 时向上"的规则(正数场景),与 Pythonround的"银行家舍入"行为略有差异,但本题所有用例的第三位小数都不触发差异点,两版结果一致。 - 公式本身的非线性:飞行时长以 0.1 倍率线性折算进结果,意味着 15 小时飞行无论向东还是向西都贡献 1.5 或 1.5×1.5 小时的加成——理解这一点有助于你在类似"权重公式 + 阈值判定"的题目中快速建立模型。
- 进一步练习:可以自行扩展城市表(例如增加 UTC+5:30 的德里、UTC+1 的巴黎)、支持"同名城市返回 0"、或把结果改为向上取整到半小时,以此加深对查表与舍入边界的理解。
参考资源
- 挑战原文(JS 版):curriculum/challenges/english/blocks/daily-coding-challenges-javascript/6a0dcd03ee4e68698080ef66.md
- 挑战原文(Python 版):curriculum/challenges/english/blocks/daily-coding-challenges-python/6a0dcd03ee4e68698080ef66.md
- 块配置:curriculum/structure/blocks/daily-coding-challenges-javascript.json
- 超级块配置:curriculum/structure/superblocks/dev-playground.json
- 后端按日期拉取接口:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts
- 前端数据校验:client/src/utils/daily-coding-challenge-validator.ts
- 前端日期/时区辅助:client/src/components/daily-coding-challenge/helpers.ts
- 前端展示组件:client/src/components/daily-coding-challenge
【免费下载链接】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),仅供参考