首页 / 资讯中心 / 文章详情

上周最后一天怎么算?Python、SQL、Shell多语言日期计算实战

上周最后一天怎么算?Python、SQL、Shell多语言日期计算实战 ★ FEATURED ARTICLE
你有没有遇到过这样的需求做报表统计、任务调度或者数据清洗时经常要算“上周最后一天”。听起来特别简单不就是减几天的事嘛可实际动手时今天周几、系统时区、跨月月初这些因素混在一起很容易把人绕晕。我去年做数据仓库的日活统计时就因为在“上周最后一天”这个日期上算错了一天导致一周的报表数据全对不上排查了两个小时才发现是日期边界的问题。所以这个看似基础的问题很值得认真聊一聊。这篇文章我要分享的内容适合正在写数据处理逻辑的开发、做报表的运营分析以及维护定时任务的 DBA 或运维同学。我会从需求定义开始给出通用算法再用 Python、SQL、Shell 三种语言完整落地最后把边界情况和工程化封装也一并说透。读完你不仅知道怎么写还能理解每步计算背后的原因以后再遇到类似日期需求就不会慌。1. 需求拆解先搞清楚“上周最后一天”是哪一天1.1 三种常见的业务定义在正式写代码前我习惯先问自己一句这里的“上周最后一天”在业务上到底指哪一天不同场景下答案完全不同。第一种也是最常见的是自然周定义一周从周一到周日上周最后一天就是上周日。国内大多数互联网公司的运营报表、用户行为统计用的都是这个定义。你去看各种数据分析平台的周报默认的“上周”范围基本都是周一至周日。第二种是财务口径或部分习惯使用“周日到周六”为周的地区一周从周日开始到周六结束那么上周最后一天是上周六。这种情况在外企、跨境电商、某些财务结算系统里很常见。第三种是 ISO 8601 标准也是很多国际通用系统的默认规则每周从周一开始到周日结束。这与第一种本质上是一样的都认为一周最后一天是周日区别只在起始日和编号规则上。所以你看不考虑业务背景直接写代码很可能写出来的结果跟预期差一天。我自己第一次实现时就是直接减 7 天结果在周一跑任务时拿到的还是本周的日期后面才意识到不能这么简单处理。1.2 为什么不能直接“今天减7天”很多人直觉上觉得上周最后一天不就是当前日期减 7 天吗但如果今天是周四减 7 天是上周四根本不是上周日。这个方案只有在“今天”恰好是这周最后一天的时候才成立而真实定时任务一般每周都会跑总会遇到非最后一天的情况。更深层的原因在于日期的“周”是由星期几来刻画的一组离散编号而不是纯粹的按 7 天滚动。我们要找的不是“7 天前”而是“距离今天最近的一个已经完整过去的周的最后一天”。要定位它必须先知道今天是星期几然后从今天往回推到“本周第一天”再往前一天才是上周最后一天。这个逻辑链路清楚了代码怎么写都不会错。2. 核心实现逻辑基于“今天是星期几”的通用算法2.1 把计算步骤拆成四条公式以“上周最后一天 上周日”为例拆解后的步骤是这样的获取今天是星期几的数值记为w。根据周的起始日计算今天到“本周第一天”的偏移量。用今天 - 偏移量得到本周第一天的日期。用本周第一天 - 1天得到上周最后一天。如果采用周一作为一周的第一天w计数为周一0周二1……周日6那么今天到本周一的偏移量就是w。本周一往前一天就是上周日。因此最终公式为上周日 今天 - (w 1)天举个例子今天是周三w 2那么今天 - 3天就是上周日。如果用日历验证今天是 2025 年 4 月 16 日星期三减 3 天是 4 月 13 日星期日完全正确。如果业务定义为“上周最后一天 上周六”即周从周日起始周六结束公式变成了上周六 今天 - (w 2)天为什么是w 2因为在这种情况下本周的起点是周日从今天到本周日的偏移量需要额外加 1 天。公式我会放在后面的工程化函数里统一处理这里先把原理讲明白。2.2 用 Python 写第一版函数Python 标准库的datetime模块已经足够我们实现不需要额外装包。第一版代码可以这么写from datetime import date, timedelta def get_last_sunday(todayNone): 获取上周日周一作为一周的第一天 参数: today: 日期对象默认为当天 返回: 上周日的日期对象 if today is None: today date.today() # today.weekday() 返回 0-6对应周一至周日 weekday today.weekday() offset weekday 1 return today - timedelta(daysoffset)这段代码的核心就在于offset weekday 1。我来解释一下这个1weekday是今天距离本周一的天数而本周一减 1 天才到上周日所以总共要多减一天。今天如果是周一weekday 0那么上周日就是昨天看起来没问题今天如果是周日weekday 6那么 offset 7返回 7 天前也正是上周日不会把今天的日期算进去。我个人建议在实际代码里把weekday的计算步骤分开写并加注释。因为日期逻辑容易出隐性 bug注明每一步的含义能帮自己和后来者省很多时间。3. 多语言实战SQL、Shell 与其他工具怎么算3.1 SQL 里的两种常用写法数据库是统计需求的重灾区多数时候我们并不想在应用层查出所有原始数据再处理而是希望直接用 SQL 在查询里算出日期边界。以 PostgreSQL 为例最简单的写法是利用EXTRACT(ISODOW FROM CURRENT_DATE)它返回 1 到 7正好对应周一至周日-- 上周日先回退到本周一再往前一天 SELECT CURRENT_DATE - EXTRACT(ISODOW FROM CURRENT_DATE)::int - 1;其中CURRENT_DATE - EXTRACT(...)::int得到的就是本周一再减 1 就是上周日。PostgreSQL 还有一个更语义化的写法SELECT date_trunc(week, CURRENT_DATE)::date - 1;date_trunc(week, ...)会把日期截断到本周一然后转成日期类型再减 1 天。这个写法更直白性能也更好。MySQL 的WEEKDAY()函数返回的是 0 到 6对应周一到周日因此写法为SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) 1 DAY);如果你用 Snowflake可以这样SELECT DATEADD(day, -DAYOFWEEKISO(CURRENT_DATE) - 1, CURRENT_DATE);要注意不同数据库的“星期几”编号起点不一样这是最容易踩坑的一步。Oracle 的TO_CHAR(date, D)会受会话参数影响返回数字不固定建议用TRUNC(date, IW) - 1来得到上周一或上周日避免依赖具体语言环境。3.2 Shell 脚本里的实用技巧在写 cron 定时任务或 Shell 脚本时我们经常要动态拼日期。GNUdate命令非常强大可以直接用last sunday这种人类可读的写法last_sunday$(date -d last sunday %Y-%m-%d) echo $last_sunday但这里有个隐蔽的坑date -d last sunday的行为取决于 GNU date 的版本。有些版本在今天是周日时last sunday返回的是 7 天前而不是今天。这在大多数情况下正好是我们要的“上周日”但仍建议通过%u参数显式计算避免歧义。date %u返回 1 到 7对应周一至周日。要得到上周日可以直接这样算# 今天距离上周日的天数 $(date %u) last_sunday$(date -d -$(date %u) days %Y-%m-%d)验证一下如果今天是周四%u为 4date -d -4 days输出的是 4 天前也就是上周日。如果今天是周一%u为 1减 1 天就是昨天周日。这个式子永远成立。如果你嫌date -d在不同 Unix 系统上兼容性差可以借助 Python 或者 Perl 来生成日期字符串再交还给 Shell 使用。例如last_sunday$(python3 -c from datetime import date, timedelta d date.today() print(d - timedelta(daysd.weekday() 1)) )3.3 不同工具方案的速查对比为了方便你在不同环境里快速选取方案我把常见的日期计算口径整理成了一张表环境推荐写法关键点Python 标准库today - timedelta(daystoday.weekday() 1)weekday()周一0周日6PostgreSQLdate_trunc(week, CURRENT_DATE)::date - 1week从周一开始MySQLDATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) 1 DAY)WEEKDAY()周一0周日6SnowflakeDATEADD(day, -DAYOFWEEKISO(CURRENT_DATE) - 1, CURRENT_DATE)基于 ISO 周GNU Shelldate -d -$(date %u) days %Y-%m-%d%u周一1周日7dateutiltoday relativedelta(weekdaySU(-1))需要额外安装 python-dateutil表格里 dateutil 的写法我单独说一下。relativedelta(weekdaySU(-1))表示最近的“上一个周日”如果今天是周三它返回上周日如果今天是周日它返回 7 天前。这个语义与“上周日”刚好一致所以也可以使用。但如果你习惯标准库不依赖第三方包会更省心。4. 边界场景与隐蔽坑位跨年、月初、时区4.1 跨年和月初并没有想象中可怕很多人担心如果今天是 1 月 1 号或者 1 月 2 号算上周日会不会算出负数然后出错我用 Python 的timedelta做了测试比如 2025 年 1 月 1 日是周三weekday()是 2减去 3 天得到 2024 年 12 月 29 日正好是上周日跨年完全没问题。日期库内部实现的是日期的算术运算它会正确退回到上个月最后几天。真正需要担心的反而是我们自己的业务逻辑里有没有写死“年份”或“月份”值。比如有人喜欢先取“上月最后一天”再去算上一周这种间接做法容易出错不如直接基于当天做日期加减。4.2 时区和运行环境是隐形杀手我遇到过一个非常典型的问题服务器部署在某个海外机房时区设置没统一导致凌晨跑任务时拿到的“今天”还是 UTC 时间的前一天。比如国内已经过了周一的零点但服务器时区还是 UTC系统日期仍然显示周日那么算出来的“上周日”就少退了 7 天落到上周日当天数据直接错乱。解决方案是在所有涉及日期计算的模块里统一指定时区。比如 Python 中明确使用本地时区或一个固定的业务时区from datetime import datetime, timezone, timedelta # 假设业务时区为 UTC8 tz timezone(timedelta(hours8)) today datetime.now(tz).date()更稳妥的做法是在配置中心或环境变量里定义“业务日期”而不是“系统日期”由调度框架统一注入。我在团队里通常会要求所有定时任务不要直接调用date.today()而是用任务执行时传入的批次日期这样测试和生产环境下结果才不会漂移。4.3 周起始日不一致导致的差异如果两类报表用了不同的周起始日哪怕代码逻辑完全相同算出来的“上周最后一天”也可能差一天。举个例子A 报表用“周一为一周第一天”B 报表用“周日为一周第一天”。在某个周二去计算“上周最后一天”A 报表得到的是 3 天前的周日B 报表得到的是 4 天前的周六。两者中间隔了一个周日很多数据分析师如果没意识到这一点就会产生核对不一致。所以在团队协作中我建议把“周的起始日”定义成项目级别的全局常量并且在查询前打印出来或者直接体现在函数参数名里。比如函数叫get_last_day_of_last_week(week_starts_on1)就会比get_last_sunday()清晰得多。5. 把逻辑工程化写一个可复用的通用函数5.1 用参数控制不同周的起始日成熟的代码不应该只在单一场景下正确至少要把“周起始日”和“期望返回周几”这两个变量暴露出来。下面这份代码是我在实际项目中用过的简化版升级了上面的第一版函数from datetime import date, timedelta def get_last_day_of_last_week(todayNone, week_start0, last_weekdayNone): 获取上周最后一天的日期。 参数: today: 当前日期默认为 date.today() week_start: 一周第一天在 python.weekday() 中的数值0周一6周日 last_weekday: 希望返回的星期几默认本周最后一天即 week_start 的前一天 返回: 日期对象 if today is None: today date.today() # 当前星期几距离 week_start 的天数 offset_to_start (today.weekday() - week_start) % 7 this_week_start today - timedelta(daysoffset_to_start) # 如果未指定 last_weekday最后一天是 week_start 前一天 if last_weekday is None: last_weekday (week_start - 1) % 7 # 求 last_weekday 距离 week_start 的天数 days_ahead (last_weekday - week_start) % 7 last_week_end this_week_start - timedelta(days1) # 再往后推到指定的 last_weekday # 但实际上我们只要上周最后一天所以这里直接返回 this_week_start - 1 天即可 # 如果需要任意 weekday再根据 days_ahead 调整 return last_week_end等等这个函数被我写复杂了。让我重新整理一下保持简单还容易理解。更清晰的做法是直接定义两种策略from datetime import date, timedelta def last_sunday(todayNone): 周一为一周之首上周最后一天是周日 if today is None: today date.today() return today - timedelta(daystoday.weekday() 1) def last_saturday(todayNone): 周日为一周之首上周最后一天是周六 if today is None: today date.today() # 当今天为周日时weekday()6需要偏移 7 天才能回到上周六 # 当今天为周一时weekday()0需要偏移 2 天 # 规律是offset weekday() 2 return today - timedelta(daystoday.weekday() 2)这个版本虽然函数简单但语义非常明确不容易出错。如果业务上只有这两种口径优先用这种直白的方式。只有当天数口径特别多时才引入通用参数。5.2 用测试固定日期行为日期逻辑最容易在“未来某天”出问题所以我的习惯是把测试用例写全。比如用已知的固定日期来验证计算结果from datetime import date def test_last_sunday(): # 2025-04-16 是周三上周日应该是 2025-04-13 assert last_sunday(date(2025, 4, 16)) date(2025, 4, 13) # 2025-04-13 是周日上周日应该是 2025-04-06 assert last_sunday(date(2025, 4, 13)) date(2025, 4, 6) # 2025-01-01 是周三跨年场景上周日是 2024-12-29 assert last_sunday(date(2025, 1, 1)) date(2024, 12, 29)有了这些测试以后改代码或者升级依赖库时只要测试一跑就能瞬间发现日期逻辑有没有被破坏。我强烈建议把这类测试纳入 CI因为统计系统的日期计算很少会有人手动核对自动化测试就是安全网。6. 实操心得数据统计场景的复盘与排查技巧6.1 在真实报表里怎么用最稳妥做数据统计时我通常不会仅仅在 SQL 里写一句日期条件就完事而是把起始日期和结束日期都显式列出来放在查询的第一行。比如WITH date_range AS ( SELECT CURRENT_DATE - EXTRACT(ISODOW FROM CURRENT_DATE)::int - 1 AS start_date, CURRENT_DATE - EXTRACT(ISODOW FROM CURRENT_DATE)::int AS end_date ) SELECT * FROM event_table, date_range WHERE event_day start_date AND event_day end_date -- 注意是左闭右开避免漏掉上周日当天 ;这里我用了“上周一作为起始日期本周一作为结束日期”的区间方式因为区间条件用左闭右开[,)可以避免日期时间类型里含有时分秒的边界问题。如果要统计“上周最后一天”当天直接取start_date即可但更多场景其实是整周区间。6.2 排查日期问题常用的三步法如果你发现报表里日期不对劲先别急着改代码按这三步排查效率最高第一步确认“今天”的日期到底是多少。不要相信直觉直接在运行环境里执行date %F或SELECT CURRENT_DATE排除时区影响。第二步确认周的起点定义。查看代码里的注释或者配置看它认为的一周第一天是周一还是周日。可以在测试环境打印如下中间值今天是星期几、偏移量是多少、最终算出哪一天。我之前排查问题时就通过打印这两个输出很快定位到是weekday()和业务定义相差一天。第三步用已知的固定日期做单测。比如用周一和周日的两个日期分别调用函数观察结果是否符合预期。如果两个关键日期的输出都正确那么其他日期也只是线性偏移基本不会有问题。最后分享一个我个人的小习惯所有和“周”相关的日期函数我都会在函数名或注释里写清业务口径。比如get_last_sunday_based_on_monday_week()名字长一些没关系重点是让三个月后的自己翻代码时能一眼看明白这函数不是为了“周日的技术实现”而是为了“周一起始的周报统计”。日期代码里最大的隐患往往不是算法写不出来而是算法写出了没人能确认它到底在算什么。希望这篇内容能帮你少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站