Считайте количество дней между двумя датами, не считая выходных и праздников - PullRequest
1 голос
/ 06 мая 2020

У меня есть эти даты, 02.04.2020 и 30.06.2020, и я хочу проверить, сколько дней между ними, пропуская указанные даты, например 25 декабря или 1 мая, а также выходные.

Например, между двумя указанными выше датами 147 дней (конечная дата здесь не учитывается), но между этими датами 21 выходной, так что всего 105 рабочих дней. И если пятница 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 голосов
/ 06 мая 2020

Если у вас есть список дат, которые следует пропустить, вы можете проверить, попадает ли какая-либо из них в диапазон дат начала и окончания. Объекты 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 находится между двумя значениями. Для ваших примеров дат это будет только в случае 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

Если ваш список дат, которые нужно пропустить, большой , обработка вышеуказанного может занять слишком много времени, тестирование каждая дата в списке по отдельности не так эффективна.

В этом случае вы хотите использовать деление пополам , чтобы быстро определить количество совпадающих дат между start и end , убедившись, что список пропущенных дат хранится в отсортированном порядке , а затем с помощью модуля bisect найдите индексы, в которые вы должны вставить start и end; разница между этими двумя индексами - это количество совпадающих дат, которые вы хотите вычесть из своего счетчика диапазона:

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 дат попадают в диапазон от start до end, в то время как упрощенный c for to_skip in dates_to_skip: l oop выше занимает O (N) шагов. Неважно, есть ли 5 ​​или 10 дат для тестирования, но если у вас 1 тысяча дат, тогда начинает иметь значение, что для метода bisect требуется только 10 шагов, а не 1 тысяча.

Обратите внимание, что ваш подсчет выходных: неверно , это слишком упрощенно c. Вот пример, который показывает, что количество выходных дней различается для двух разных периодов по 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 дней.

Следующая функция реализует эти уловки, чтобы вы могли получить нужное количество выходных дней между любыми двумя датами 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)

Логи настройки c может потребовать дополнительных пояснений. Сдвигать обе границы вперед вместо того, чтобы перемещать начало вперед и конец назад во времени, гораздо проще; не требуется никаких других корректировок в подсчетах, в то же время делая тривиальным подсчет целых выходных между двумя датами.

Если оба start и end - будние дни (результат их метода date.weekday() - значение от 0 до 4), то при переходе к следующей субботе между двумя датами будет сохраняться такое же количество полных выходных, независимо от того, в какой день недели они начинались. Перенос дат таким образом не искажает количество выходных дней, но значительно упрощает получение правильного числа.

Если start выпадает на воскресенье, для перехода на следующую субботу потребуется учитывать это пропущенное воскресенье отдельно; это половина выходных, которую вы хотите включить в результат, поэтому вы хотите добавить 1 к общей сумме. Если end выпадает на воскресенье, тогда этот день не должен учитываться в общем (конечная дата является исключительной в диапазоне), но переход на следующую субботу будет включать это в счетчике, поэтому вы хотите вычесть этот дополнительный выходной день.

В приведенном выше коде я просто использую два логических теста с вычитанием для вычисления начального значения boundary_sundays. В Python тип bool является подклассом int, а False и True имеют целочисленные значения. Вычитание двух логических значений дает целочисленное значение. boundary_sundays будет -1, 0 или 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

Демо:

>>> 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
...