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

时间:2020-05-06 11:58:04

标签: python datetime timedelta

我有这些日期,2020年2月2日和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)

如何从该计数中删除假期日期?

1 个答案:

答案 0 :(得分:0)

如果您有应该跳过的日期的列表,则可以测试是否有任何日期在开始日期和结束日期范围内。 date个对象是可排序的,因此可以使用:

# 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

仅当start <= to_skip < end日期介于两个值之间时,to_skip链接比较才为真。对于您的示例日期,只有5月1日是这种情况:

>>> from datetime import date
>>> start = date(2020, 2, 4)
>>> end = date(2020, 6, 30)
>>> dates_to_skip = [date(2020, 5, 1), date(2020, 12, 25)]
>>> for to_skip in dates_to_skip:
...     if start <= to_skip < end:
...         print(f"{to_skip} falls between {start} and {end}")
...
2020-05-01 falls between 2020-02-04 and 2020-06-30

如果您要跳过的日期列表为,则上述操作可能会花费很长时间,因此单独测试列表中的每个日期并不是那么有效。

在这种情况下,您要使用 bisection 来快速确定startend之间的匹配日期数,方法是确保保留要跳过的日期列表按照排序,然后使用bisect module查找要插入startend的索引;这两个索引之间的差异是您要从范围计数中减去的匹配日期数:

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()为您提供所有值所在的索引 dates_to_skip[start_idx:]中的等于或大于开始日期。对于结束日期,dates_to_skip[:end_idx]中的所有值都将较低(dates_to_skip[end_idx]本身可以等于end,但排除end)。而且,一旦您知道开始日期的索引,当搜索结束日期的索引时,我们可以告诉bisect_left()跳过所有值,直到start_idx,因为结束日期将比任何start值(尽管dates_to_skip[start_idx]处的值可能高于开始值和结束值)。这两个bisect_left()结果之间的区别是开始日期和结束日期之间的日期数。

使用bisect的优势在于,它需要O(logN)步骤来计算N个日期列表中有多少个日期落在startend之间,而这很简单上方的for to_skip in dates_to_skip:循环需要O(N)个步骤。有5个或10个要测试的日期无关紧要,但是如果您有1000个日期,那么bisect方法仅需要10个步骤,而不是1000个就变得很重要。

请注意,您的周末计数计算不正确,这太简单了。这是一个示例,显示周末日期的数量在11天的两个不同时段中有所不同。对于任何一个示例,您的方法都将需要2个周末天:

假设您的开始日期是一个星期一,结束日期是又一个星期的星期五,您之间只有1个周末,因此有11-2 = 9个工作日(不计算结束日期):

| M   | T | W | T | F   |  S  |  S  |
|-----|---|---|---|-----|---- |-----|
| [1] | 2 | 3 | 4 |  5  | _1_ | _2_ |
|  6  | 7 | 8 | 9 | (E) |     |     |

在上表中,[1]是开始日期,(E)是结束日期,数字表示工作日;跳过的周末数以_1__2_个数字计算。

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

| M | T   | W | T | F   |  S  |  S  |
|---|-----|---|---|-----|-----|-----|
|   |     |   |   | [1] | _1_ | _2_ |
| 2 |  3  | 4 | 5 |  6  | _3_ | _4_ |
| 7 | (E) |   |   |     |     |     |

因此,计算开始和结束之间的天数,然后将该数字除以7,这不是在此处计算星期或周末的正确方法。要计算整个周末,请查找从开始和结束日期算起最近的星期六(向前),这样您最终得到两个相隔7天的倍数的日期。将该数字除以7,即可得出两天之间整个周末的实际数字。然后,如果开始日期或结束日期是在移动之前的星期日,则调整该数字(在星期日开始时,将总数加一,结束日期为星期日,则从总数中减去一日)。

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

以下函数实现了这些技巧,以使您在两个日期(7 - 1)start之间(其中end小于start)正确的周末天数:< / p>

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方法的结果是0到4之间的值),则移动到下一个星期六将保持整个周末的相同数量在两个日期之间,无论他们从哪个工作日开始。以这种方式向前移动日期不会使周末的日数产生偏差,但确实可以更轻松地获取正确的数字。

如果date.weekday()落在某个星期日,则向前移动到下一个星期六将需要分别说明该跳过的星期日;您希望将半个周末包含在结果中,以便将总数加1。如果start是星期天,则该天不应该计入总数(结束日期在该范围内),但是要移至下一个星期六 / em>将其包括在计数中,因此您要减去这多余的周末。

在上面的代码中,我仅使用两个布尔测试加上减法来进行初始end值的计算。在Python中,boundary_sundays类型是bool的子类,并且intFalse具有整数值。减去两个布尔值将得到一个整数值。根据发现的星期日数,True将是boundary_sundays-10

将它们放在一起:

1

演示:

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