freeCodeCamp 每日编程挑战解析:用 Python 计算距离周末的天数(Challenge 96: Is It the Weekend?)
本篇技术指南围绕 freeCodeCamp 开源仓库中 Python 每日编程挑战第 96 题 "Is It the Weekend?" 展开,完整拆解题目的输入输出规则、官方测试用例、种子代码与标准解法,并结合仓库中的课程结构、双语挑战校验测试、数据种子脚本与 API 路由,深入讲解 datetime 日期解析、星期编号、时区无关计算的底层原理。读完本文,你既能独立通过这道挑战,也能理解它在 freeCodeCamp 每日编程挑战体系中的完整运行链路。
挑战背景:freeCodeCamp 每日编程挑战体系
这道题属于仓库中 dev-playground 超级块(superblock)下的 daily-coding-challenges-python 块,是共 365 道每日挑战中的第 96 道。它的块配置定义在 curriculum/structure/blocks/daily-coding-challenges-python.json 中,关键元数据如下:
isUpcomingChange: true:标记为即将上线的新内容,需要启用相应环境变量(SHOW_UPCOMING_CHANGES)才能在客户端显示;usesMultifileEditor: true:挑战使用多文件编辑器;helpCategory: "Python":归入 Python 帮助分类;blockLayout: "legacy-challenge-list":按传统挑战列表布局展示。
值得注意的是,这套每日挑战是双语配对的:每个挑战同时存在 JavaScript 与 Python 两个版本,二者共享同一个挑战 ID。本道题的 JS 版本位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68f6587287ad1f4ad39b0c82.md,其函数名为 daysUntilWeekend,而 Python 版本(即本文主题)的函数名为 days_until_weekend。仓库中的测试 curriculum/src/test/daily-challenges.test.js 会严格校验 JS 与 Python 两套挑战的数量一致、ID 一致,且在英语环境下标题、描述与测试数量也必须完全匹配——这就是题目规则双语完全对等的工程保障。
题目要求:返回距离周末的天数
题目本身是一道典型的日期与星期运算练习,原文规则如下:
Given a date in the format
"YYYY-MM-DD", return the number of days left until the weekend.
逐条拆解输入输出契约:
- 输入是一个形如
"YYYY-MM-DD"的字符串,例如"2025-11-14"; - 周末从星期六开始(这是规则定义的关键:即使星期日也被视为"周末",但倒计时目标始终锚定在星期六);
- 如果给定日期本身就是星期六或星期日,直接返回
"It's the weekend!"; - 其他情况返回
"X days until the weekend.",其中X是距离下一个星期六的天数; - 当
X等于1时,必须使用单数形式"day",而不是复数"days"; - 计算必须忽略本地时区——这是本题真正的隐藏考点,防止不同时区的运行环境得到不同结果。
用一句话概括核心逻辑:把字符串解析成"纯日期"(不带时区),算出它是星期几,然后分"周末/非周末"两条分支处理。
官方测试用例:六组断言全览
题目通过 # --hints-- 提供了六组由 unittest 驱动的测试断言,每组都在 runPython 环境中执行 TestCase().assertEqual(...) 对函数返回值做精确比对。这些用例覆盖了周一至周日全部七种星期情形以及单复数切换:
| 输入日期 | 星期 | 期望输出 |
|---|---|---|
"2025-11-14" |
星期五 | "1 day until the weekend." |
"2025-01-01" |
星期三 | "3 days until the weekend." |
"2025-12-06" |
星期六 | "It's the weekend!" |
"2026-01-27" |
星期二 | "4 days until the weekend." |
"2026-09-07" |
星期一 | "5 days until the weekend." |
"2026-11-29" |
星期日 | "It's the weekend!" |
以第一个断言为例,其原始测试代码为:
({test: () => { runPython(`
from unittest import TestCase
TestCase().assertEqual(days_until_weekend("2025-11-14"), "1 day until the weekend.")`)
}})
这套测试设计覆盖了三个关键点:周五边界(结果为 1,触发单数形式)、工作日普遍情形(周三、周二、周一)、周六与周日两个周末分支(返回固定文案)。注意最后一条 "2026-11-29" 是星期日,它同样返回 "It's the weekend!",验证了"周六或周日都算周末"的判定。
种子代码:从函数骨架开始
题目为学习者提供的起点(--seed--)极其精简,只定义了函数签名与占位返回:
def days_until_weekend(date_string):
return date_string
你需要在这个骨架内补充完整实现,替换掉占位的 return date_string。date_string 是唯一入参,函数必须返回题目规定的三种输出之一(周末文案、单数天数字符串、复数天数字符串)。
官方参考解法:逐步解析
题目 # --solutions-- 给出的参考实现如下:
from datetime import datetime
def days_until_weekend(date_string):
date = datetime.strptime(date_string, "%Y-%m-%d").date()
day_of_week = date.weekday()
if day_of_week == 5 or day_of_week == 6:
return "It's the weekend!"
days_until_saturday = 5 - day_of_week
return f"{days_until_saturday} day{'s' if days_until_saturday != 1 else ''} until the weekend."
逐行拆解其设计思路:
第 1 步:解析字符串为纯日期。 datetime.strptime(date_string, "%Y-%m-%d") 按格式指令 %Y-%m-%d(四位年份-两位月份-两位日期)把字符串解析为 datetime 对象,紧接着调用 .date() 剥离时间部分,得到 datetime.date 对象。date 是不带时区、不带时分秒的纯日历日期,这正是"忽略本地时区"的实现基础——后面会详细展开。
第 2 步:取得星期编号。 date.weekday() 返回一个 0 到 6 的整数:0 代表星期一,1 代表星期二……5 代表星期六,6 代表星期日。
第 3 步:周末分支。 当 day_of_week 等于 5(周六)或 6(周日)时,直接返回固定文案 "It's the weekend!"。这里没有用 in (5, 6) 的写法而是拆成两个 or 条件,只是风格差异,逻辑等价。
第 4 步:计算倒计时。 days_until_saturday = 5 - day_of_week。由于 weekday() 的周六编号是 5,对于周一至周五(编号 0–4),这个差值恰好是"到下一个周六还差几天":周一 5 天、周二 4 天、周三 3 天、周四 2 天、周五 1 天。
第 5 步:单复数处理。 f"{days_until_saturday} day{'s' if days_until_saturday != 1 else ''} until the weekend." 是一个条件表达式嵌套在 f-string 中:当 days_until_saturday 不等于 1 时拼接 s,等于 1 时留空,从而得到 "1 day until the weekend." 而非 "1 days ..."。六组测试中唯一的单数情形是周五 "2025-11-14",正是对这条分支的验证。
时区无关性:这道题的隐藏考点
题目规则最后一条强调 "Make sure the calculation ignores your local timezone",这是最容易踩坑的地方。理解这一点需要区分 Python 中三类日期时间对象:
datetime:既含日期又含时间,可能带 tzinfo(时区信息),也可能不带(naive);datetime.date:只有年月日,根本没有时区概念,因此它的星期编号在任何运行环境下都一致;datetime(带 tzinfo)+astimezone():会受本地时区影响,如果直接对带时区的对象取星期几,同一 UTC 时刻在不同时区可能落在不同日历日。
参考解法通过两步消除时区影响:strptime 按固定格式解析出的 datetime 本身是 naive 的,.date() 再进一步丢弃时间部分。此后 weekday() 完全由日历本身决定,与进程运行在 UTC、北京、纽约还是芝加哥无关。
对照 JS 版本可以看得更清楚,其官方解法是:
function daysUntilWeekend(dateString) {
const [year, month, day] = dateString.split("-").map(Number);
const date = new Date(Date.UTC(year, month - 1, day));
const dayOfWeek = date.getUTCDay();
if (dayOfWeek === 6 || dayOfWeek === 0) {
return "It's the weekend!";
}
const daysUntilSaturday = 6 - dayOfWeek;
return `${daysUntilSaturday} day${daysUntilSaturday === 1 ? "" : "s"} until the weekend.`;
}
这里用 Date.UTC(...) 构造时间戳再用 getUTCDay() 取星期,同样是为了绕开 getDay() 的本地时区偏移。注意两版星期编号体系不同:Python weekday() 以周一为 0(周一 0 … 周日 6),JS getUTCDay() 以周日为 0(周日 0 … 周六 6),所以 Python 版周末判定是 5 or 6、倒计时用 5 - weekday,而 JS 版周末判定是 6 or 0、倒计时用 6 - getUTCDay。逻辑一一对应,只是编号基准不同,这也解释了为什么两版测试断言完全一致。
边界情况与易错点分析
基于测试用例与解法,可以归纳出四类边界情况,供自行验证时对照:
- 周五 → 1 天:星期五编号为
4,5 - 4 = 1,输出单数"1 day until the weekend."。这是唯一触发单数形式的工作日。 - 周六/周日 → 周末文案:编号
5、6直接命中分支,输出"It's the weekend!",不再计算天数。 - 周一 → 5 天:编号
0,5 - 0 = 5,即从周一开始需要跨越周二、周三、周四、周五才到周六。 - 字符串格式严格性:
strptime按"%Y-%m-%d"严格解析,"2025-1-1"(月份/日期无前导零)或"2025/01/01"这类格式会抛出ValueError。六组测试全部使用标准格式,因此可以依赖严格解析,但若需防御性处理,可以提前用正则校验格式。
另外值得注意:题目描述虽然要求"忽略本地时区",但在 freeCodeCamp 的前端展示链路中,日期本身是与**美国中部时区(America/Chicago)**绑定的——客户端组件 client/src/components/daily-coding-challenge/helpers.ts 通过 toZonedTime(dateObj, 'America/Chicago') 获取"今日"并格式化为 yyyy-MM-dd 后请求对应挑战。也就是说,"哪一天是哪道题"由美国中部时区决定,而"这道题算出来是星期几"则完全由日历决定,两者并不冲突。
挑战在完整系统中的运行链路
这道题不只是孤立的练习,它在 freeCodeCamp 仓库中拥有完整的生产链路,理解这条链路有助于把握题目的工程背景:
-
课程编写:挑战以 Markdown 形式维护在 curriculum/challenges/english/blocks/daily-coding-challenges-python/ 目录下,frontmatter 中的
id(68f6587287ad1f4ad39b0c82)、challengeType(本 Python 题为29,对应 JS 版为28)与dashedName用于课程解析。 -
双语配对校验:curriculum/src/test/daily-challenges.test.js 在
SHOW_UPCOMING_CHANGES=true环境下从dev-playground超级块加载挑战,逐一比对 JS/Python 两版的 ID、标题、描述与测试数量,防止两套课程内容漂移。 -
数据种子:tools/daily-challenges/seed-daily-challenges.ts 从 2025-08-11 起按每天一道的节奏,把 365 道挑战写入 MongoDB 的
DailyCodingChallenges集合。其辅助模块 tools/daily-challenges/helpers.ts 通过 GraphQL(端点http://localhost:8000/___graphql)从正在运行的客户端拉取挑战,并用combineChallenges把 JS/Python 两版合并为一条同时含javascript与python字段的记录;脚本内置了EXPECTED_CHALLENGE_COUNT = 365的校验,并锁定起始日期(若被改动会直接抛错)。完整操作步骤见 tools/daily-challenges/README.md。 -
API 提供:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 暴露了
GET /daily-coding-challenge/date/:date、/day/:day、/today、/month/:month、/all、/newest等公开路由,按日期从 Prisma 查询挑战,且对"晚于美国中部今日"的日期返回 404;本道题对应的日期落在 2025-11-14 之后(第 96 道,即 2025-11-14 当天,因为种子从 2025-08-11 起每天一道,2025-08-11 + 95 天 = 2025-11-14)。 -
前端渲染:client/src/client-only-routes/show-daily-coding-challenge.tsx 从 API 取回挑战数据后,将其转换为
show-classic挑战模板所需的格式(补上<section id="description">包装、设置superBlock: 'daily-coding-challenge'、usesMultifileEditor: true等),用户在页面上就能用多文件编辑器编写days_until_weekend并通过测试。
自行验证:本地运行与测试
如果你想在本地亲手验证本题解法,可以按以下方式操作(仓库为只读,仅用于运行验证):
- 使用任一 Python 3 环境,将官方解法保存为脚本;
- 在脚本末尾追加六组断言对应的
unittest用例,例如:
from unittest import TestCase
TestCase().assertEqual(days_until_weekend("2025-11-14"), "1 day until the weekend.")
TestCase().assertEqual(days_until_weekend("2025-01-01"), "3 days until the weekend.")
TestCase().assertEqual(days_until_weekend("2025-12-06"), "It's the weekend!")
TestCase().assertEqual(days_until_weekend("2026-01-27"), "4 days until the weekend.")
TestCase().assertEqual(days_until_weekend("2026-09-07"), "5 days until the weekend.")
TestCase().assertEqual(days_until_weekend("2026-11-29"), "It's the weekend!")
print("All tests passed!")
运行通过即说明实现与题目契约一致;也可以用 date -d、cal 等工具交叉核对任一日期的真实星期,验证 weekday() 编号与周六倒计时的正确性。
小结
"Challenge 96: Is It the Weekend?" 是一道麻雀虽小、五脏俱全的日期处理题:它要求你掌握 datetime.strptime 的格式解析、date.weekday() 的星期编号体系、分支逻辑与单复数文案的精确输出,并以"忽略本地时区"约束你理解纯日期与带时区时间对象的本质区别。配合 官方解法 与仓库中整套双语配对、种子、API、前端链路,你既能通过本题,也能以此为一个完整样本,理解 freeCodeCamp 课程内容从 Markdown 到线上可做题环境的全流程。
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