Python 计算两个日期之间的天数,不包括周末和节假日

Python 计算两个日期之间的天数,不包括周末和节假日,python,datetime,timedelta,Python,Datetime,Timedelta,我有2020年2月4日和2020年6月30日这两个日期,我想检查它们之间有多少天跳过了指定的日期,如12月25日或5月1日,以及周末 例如,上述两个日期之间有147天(结束日期不算在内),但这些日期之间有21个周末,因此只有105个工作日。如果5月1日星期五是假日,那么最终答案是104个工作日 我做了以下几件事来跳过周末,但我仍然不知道如何跳过假期;有没有办法创建一种“黑名单”,这样如果名单上的任何一天出现差异,就会减去一天。起初我想用字典,但我不知道怎么用 这是周末的“修复”: import

我有2020年2月4日和2020年6月30日这两个日期,我想检查它们之间有多少天跳过了指定的日期,如12月25日或5月1日,以及周末

例如,上述两个日期之间有147天(结束日期不算在内),但这些日期之间有21个周末,因此只有105个工作日。如果5月1日星期五是假日,那么最终答案是104个工作日

我做了以下几件事来跳过周末,但我仍然不知道如何跳过假期;有没有办法创建一种“黑名单”,这样如果名单上的任何一天出现差异,就会减去一天。起初我想用字典,但我不知道怎么用

这是周末的“修复”:

import math
from datetime import datetime

date_input = '4/2/2020'
date_end = '30/6/2020'
start = datetime.strptime(date_input, "%d-%m-%Y").date()
end = datetime.strptime(date_end, "%d-%m-%Y").date()

Gap = (end - start).days
N_weeks = Gap / 7
weekends = (math.trunc(N_weeks)) * 2

final_result = str((Gap) - weekends)
如何从该计数中删除假日日期?

如果您有一个应跳过的日期列表,则可以测试其中是否有任何日期在开始日期和结束日期的范围内<代码>日期对象是可订购的,因此您可以使用:

# list of holiday dates
dates_to_skip = [date(2020, 5, 1), date(2020, 12, 25)]

skip_count = 0
for to_skip in dates_to_skip:
    if start <= to_skip < end:
        skip_count += 1
如果要跳过的日期列表很大,则处理上述内容可能会花费太长时间,单独测试列表中的每个日期并不是很有效

在这种情况下,您希望使用二分法快速确定
开始
结束
之间的匹配日期数,方法是确保要跳过的日期列表按顺序排列,然后使用查找要插入
开始
结束
的索引;这两个索引之间的差异是要从范围计数中减去的匹配日期数:

from bisect import bisect_left

def count_skipped(start, end, dates_to_skip):
    """Count how many dates in dates_to_skip fall between start and end

    start is inclusive, end is exclusive

    """
    if start >= end:
        return 0
    start_idx = bisect_left(dates_to_skip, start)
    end_idx = bisect_left(dates_to_skip, end, lo=start_idx)
    return end_idx - start_idx
请注意,
bisect.bisect_left()
为您提供了所有值所在的索引 在
中,日期\u至\u跳过[start\u idx:][/code>等于或高于开始日期。对于结束日期,
dates\u to\u skip[:end\u idx]
中的所有值都将较低(
dates\u to\u skip[end\u idx]
本身可以等于
end
,但不包括
end
)。一旦知道了开始日期的索引,在搜索结束日期的索引时,我们可以告诉
bisect_left()
跳过所有值,直到
start\u idx
,因为结束日期将高于任何
start
值(尽管
dates\u to\u跳过[start\u idx]
可能高于开始和结束)。这两个结果之间的差异是开始和结束之间的日期数

使用二分法的优点是需要O(logN)个步骤来计算N个日期列表中有多少个日期介于
开始
结束
之间,而上面的to_skip in dates\u to_skip:
循环的简单方法需要O(N)个步骤。如果有5个或10个日期要测试,这并不重要,但是如果有1000个日期,那么
bisect
方法只需要10个步骤,而不是1000个步骤就开始重要了

请注意,您的周末计数计算是不正确的,它太简单了。这里有一个例子表明,在11天的两个不同时期,周末日期的数量不同;您的方法将计算两个周末,例如:

假设你的开始日期是星期一,结束日期是星期五,再过一周,你只有一个周末,因此有11-2=9个工作日(不算结束日期):

在上表中,
[1]
是开始日期,
(E)
是结束日期,数字计算工作日;跳过的周末天数以
\u 1
\u 2
数字计算

但是如果开始日是星期五,结束日是第二周的星期二,那么开始日和结束日之间的天数相同,但现在必须减去两个周末;这两天之间只有7个工作日:

| M | T   | W | T | F   |  S  |  S  |
|---|-----|---|---|-----|-----|-----|
|   |     |   |   | [1] | _1_ | _2_ |
| 2 |  3  | 4 | 5 |  6  | _3_ | _4_ |
| 7 | (E) |   |   |     |     |     |
因此,计算开始和结束之间的天数,然后将该数字除以7,并不是计算周数或周末数的正确方法。要计算整个周末,请从开始日期和结束日期中找出最近的星期六(向前),这样您就可以得到两个相隔7天的倍数的日期。将该数字除以7,将得到这两天之间整个周末的实际数量。然后,如果开始日期或结束日期在搬家前的某个星期日,则调整该数字(如果开始日期是星期日,则在总数中加一,如果结束日期是星期日,则从总数中减去一天)

你可以从任何给定的日期中找到最近的星期六,方法是取下,然后从5中减去,然后取该值模数7作为要加的天数。这将始终为您提供一周中任何给定日期的正确值;对于周末(0-4)
5-date.weekday()
是跳过到周六的天数的正数,对于周六(5)结果是0(没有可以跳过的天数),对于周日(6),
5-6
-1
,但是
%7
模运算将其转换为
(7-1)
所以是6天

下面的函数实现了这些技巧,以获得任意两个日期
start
end
之间的正确周末天数,其中
start
低于
end

from datetime import timedelta

def count_weekend_days(start, end):
    """Count the number of weekend days (Saturday, Sunday)

    Start is inclusive, end is exclusive.

    """
    if start >= end:
        return 0

    # If either start or end are a Sunday, count these manually
    # Boolean results have either a 0 (false) or 1 (true) integer
    # value, so we can do arithmetic with these:
    boundary_sundays = (start.weekday() == 6) - (end.weekday() == 6)

    # find the nearest Saturday from the start and end, going forward
    start += timedelta(days=(5 - start.weekday()) % 7)
    end += timedelta(days=(5 - end.weekday()) % 7)

    # start and end are Saturdays, the difference between
    # these days is going to be a whole multiple of 7.
    # Floor division by 7 gives the number of whole weekends
    weekends = (end - start).days // 7
    return boundary_sundays + (weekends * 2)
调整逻辑可能需要更多的解释。向前移动两个边界,而不是在时间上向前移动起点和向后移动终点,更容易处理;不需要对计数进行其他调整,同时也使得计算两个日期之间的整个周末变得微不足道

如果
start
end
都是工作日(它们的
date.weekday()
方法结果的值介于0和4之间),则无论从哪个工作日开始,移动到下一个星期六都将保持两个日期之间的整个周末数相同。移动日期
| M | T   | W | T | F   |  S  |  S  |
|---|-----|---|---|-----|-----|-----|
|   |     |   |   | [1] | _1_ | _2_ |
| 2 |  3  | 4 | 5 |  6  | _3_ | _4_ |
| 7 | (E) |   |   |     |     |     |
from datetime import timedelta

def count_weekend_days(start, end):
    """Count the number of weekend days (Saturday, Sunday)

    Start is inclusive, end is exclusive.

    """
    if start >= end:
        return 0

    # If either start or end are a Sunday, count these manually
    # Boolean results have either a 0 (false) or 1 (true) integer
    # value, so we can do arithmetic with these:
    boundary_sundays = (start.weekday() == 6) - (end.weekday() == 6)

    # find the nearest Saturday from the start and end, going forward
    start += timedelta(days=(5 - start.weekday()) % 7)
    end += timedelta(days=(5 - end.weekday()) % 7)

    # start and end are Saturdays, the difference between
    # these days is going to be a whole multiple of 7.
    # Floor division by 7 gives the number of whole weekends
    weekends = (end - start).days // 7
    return boundary_sundays + (weekends * 2)
def count_workdays(start, end, holidays):
    """Count the number of workdays between start and end.

    Workdays are dates that fall on Monday through to Friday.

    start and end are datetime.date objects. holidays is a sorted
    list of date objects that should *not* count as workdays; it is assumed
    that all dates in this list fall on Monday through to Friday;
    if there are any weekend days in this list the workday count
    may be incorrect as weekend days will be subtracted more than once.

    Start is inclusive, end exclusive.

    """
    if start >= end:
        return 0
    count = (end - start).days
    count -= count_skipped(start, end, holidays)
    count -= count_weekend_days(start, end)

    return count
>>> start = date(2020, 2, 4)
>>> end = date(2020, 6, 30)
>>> holidays = [date(2020, 5, 1), date(2020, 12, 25]  # in sorted order
>>> count_workdays(start, end, holidays)
104