算工期总把周末算进去:写个函数只算工作日
合同上写着"交付周期 15 个工作日",我掐指一算,两周多一点。结果对方说不对,得按日历数——中间夹着周末,实际是三周。
最开始我以为减掉周末就行,写完发现还有国庆、春节,以及那种"周末要上班"的调休。这篇讲怎么用 Python 算准。
一、最朴素的版本
先只用标准库,减去周末:
from datetime import date, timedelta
def workdays(start, end):
"""算 start 到 end 之间的工作日天数(含首尾)"""
count = 0
cur = start
while cur <= end:
if cur.weekday() < 5: # 0=周一 ... 4=周五,5、6 是周末
count += 1
cur += timedelta(days=1)
return count
print(workdays(date(2026, 9, 1), date(2026, 9, 15)))
date.weekday() 返回 0 到 6,周一是 0、周日是 6。所以 < 5 就是周一到周五。
这个版本处理"纯工作日"没问题,但它有个大前提:假设中间没有任何节假日。
二、节假日没法自己推
一开始我想着"把每年的节假日写进代码里不就行了",很快发现不现实:
- 春节、清明、端午、中秋这些是农历相关的,每年公历日期都不一样。
- 国庆和元旦虽然固定,但放几天、怎么调休每年由官方通知决定。
- 最麻烦的是调休:某个周六本来休息,但被调成上班日。
所以硬编码节假日列表是个维护噩梦,每年都要改,而且容易漏。
常见的做法有三种:
| 方式 | 说明 | 适合 |
|---|---|---|
| 手动维护节假日列表 | 每年更新一次 | 内部系统,能接受人工维护 |
用第三方库(如 chinesecalendar) |
库里维护好了每年的安排 | 推荐,装个包就完事 |
| 调用节假日 API | 实时但需要联网 | 有网络依赖的场景 |
个人和小项目,我建议直接用现成的库:
pip install chinesecalendar
import chinese_calendar as cc
from datetime import date
def is_workday(d):
return cc.is_workday(d)
def workdays(start, end):
count = 0
cur = start
while cur <= end:
if cc.is_workday(cur):
count += 1
cur += timedelta(days=1)
return count
它把节假日和调休都处理好了,每年更新一下库版本就行。
三、反过来:从今天起 N 个工作日是哪天
更常见的需求其实是这样:“15 个工作日之后是哪天?”
def add_workdays(start, n):
"""从 start 开始,第 n 个工作日是哪天(n=1 表示 start 本身如果是工作日就算第一天)"""
count = 0
cur = start
while count < n:
if is_workday(cur):
count += 1
if count == n:
return cur
cur += timedelta(days=1)
return cur
跑一下:
d = add_workdays(date(2026, 9, 15), 15)
print(d, d.strftime('%A')) # 看看落在星期几
起始日算不算第一天这个一定要跟业务确认清楚。有的场景"从明天开始算 15 个工作日",有的"包括今天"。差一天的事,说清楚省麻烦。
四、数据量大的时候别一天天循环
如果跨度是十年,逐天循环要跑三千多次,虽然也不慢,但可以优化一下:先按周算,再处理零头。
def workdays_fast(start, end):
# 先算完整的周
days = (end - start).days + 1
weeks, remainder = divmod(days, 7)
count = weeks * 5
cur = start + timedelta(days=weeks * 7)
for _ in range(remainder):
if cur.weekday() < 5:
count += 1
cur += timedelta(days=1)
return count
不过说实话,十年跨度也就三千多次循环,毫秒级的事,普通场景直接逐天循环完全够用,代码还清楚。别为了这点性能把逻辑搞复杂。
五、几个容易出错的地方
- 含不含首尾。
date(9,1)到date(9,1)是 1 天还是 0 天?上面我的实现是 1(因为while cur <= end)。如果业务说"间隔天数",那应该是 0。这个歧义是最常见的扯皮来源,函数名和注释里写清楚。 - 跨年要能处理。从 2026-12-20 算到 2027-01-10,别只按一个年份过滤节假日。
timedelta只知道时间差,不知道节假日。有些同学以为(end - start).days * 5 / 7能估算工作日,那是平均值,具体到某个月误差很大(比如 2 月和 10 月差很多)。date和datetime别混。datetime带时分秒,比较和加减时容易出微妙的问题。只关心日期的话用date,需要时间部分再转。- 时区。如果涉及跨时区的团队,"工作日"的定义还得看对方在哪儿。一般小项目不用管,真有需求就把所有时间统一转到一个时区再算。
- 半天假、特殊排班。这些属于考勤系统的范畴了,普通的工作日计算不管这些,遇到只能单独处理。
六、顺手做个小工具
把常用的两个功能包一下,放在项目里随时用:
from datetime import date, timedelta
import chinese_calendar as cc
def workdays_between(start, end):
"""含首尾的工作日天数"""
return sum(1 for i in range((end - start).days + 1)
if cc.is_workday(start + timedelta(days=i)))
def workdays_after(start, n):
"""start 之后第 n 个工作日(不含 start 当天)"""
cur, count = start, 0
while count < n:
cur += timedelta(days=1)
if cc.is_workday(cur):
count += 1
return cur
文章总结
算工作日这件事,代码本身不难——timedelta 逐天加、weekday() < 5 判断就行。真正的坑是节假日和调休没法自己推导,必须靠外部数据(推荐用现成库,别硬编码)。另外一个高频翻车点是首尾含不含的歧义,函数命名和注释里一定要写清楚,不然十天半个月的误差就是这么来的。写完给业务方跑几个例子对一遍确认没问题。
- 点赞
- 收藏
- 关注作者
评论(0)