簡體   English   中英

計算兩個日期之間的天數,不計算周末和節假日

[英]Count number days between two dates, not counting weekends and holidays

我有這些日期,2020 年 4 月 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)

如何從此計數中刪除假期日期?

如果您有一個應該跳過的日期列表,那么您可以測試其中是否有任何日期在您的開始日期和結束日期范圍內。 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

僅當to_skip日期介於兩個值之間時, start <= to_skip < end鏈式比較才為真。 對於您的示例日期,僅適用於 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

如果您要跳過的日期列表很大,則上述處理可能需要很長時間,單獨測試列表中的每個日期並不是那么有效。

在這種情況下,您想使用二分法快速確定startend之間的匹配日期數,方法是確保要跳過的日期列表按排序順序保存,然后使用bisect模塊查找您所在位置的索引插入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]處的值可能高於 start 和 end)。 這兩個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_數字計算。

但是如果開始日是星期五,結束日是第二周的星期二,那么開始和結束之間的整數天數相同,但現在必須減去兩個周末; 這兩天之間只有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(),然后從 5 中減去該值,然后將該值模數 7 作為要添加的天數。 這將始終為您在一周中的任何一天提供正確的價值; 對於周末天數 (0 - 4) 5 - date.weekday()是跳過到星期六的正數天數,對於星期六 (5),結果是 0(沒有天數可跳過),對於星期日 (6) , 5 - 6-1 ,但是% 7模運算將其變成(7 - 1)所以 6 天。

以下 function 實現了這些技巧,讓您在任何兩個日期startend之間獲得正確的周末天數,其中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)

調整邏輯可能需要更多解釋。 向前移動兩個邊界,而不是及時移動起點和終點,更容易處理; 不需要對計數進行其他調整,同時使計算兩個日期之間的整個周末變得微不足道。

如果startend都是工作日(它們的date.weekday()方法結果是 0 到 4 之間的值),那么移動到下一個星期六將在兩個日期之間保持相同數量的整個周末,無論他們從哪個工作日開始在。 以這種方式向前移動日期不會扭曲周末天數,但確實更容易獲得正確的數字。

如果start日期是星期日,則前進到下一個星期六需要單獨考慮這個跳過的星期天; 這是您想要包含在結果中的半個周末,因此您想要在總數中加 1。 如果end落在星期天,那么那一天不應該計入總數(結束日期在范圍內是唯一的),但是移到下一個星期六會將它包括在計數中,所以你想減去這個額外的周末.

在上面的代碼中,我簡單地使用了兩個 boolean 測試和減法來進行初始boundary_sundays周日值計算。 在 Python 中, bool類型是int的子類, FalseTrue具有 integer 值。 減去兩個布爾值會得到一個 integer 值。 boundary_sundays將是-101 ,具體取決於我們找到多少個星期日。

把這些放在一起:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM