Пересекающиеся интервалы: проверяем расписание до расчёта длительности

Длительность бронирований сложили и получили больше часов, чем существует в сутках. Несколько интервалов одного ресурса пересекаются. Сравнение начала только с концом предыдущей строки пропускает вложенные интервалы.

Установить смысл границ

Считаем интервал полуоткрытым: начало включено, конец исключён. Бронирование 10:00–11:00 и следующее с 11:00 совместимы. Если бизнес требует время на подготовку, расширьте занятый интервал по отдельному правилу. Также запретите конец раньше начала и определите допустимость нулевой длительности.

После сортировки по началу надо сравнивать новую запись со всеми предыдущими интервалами ресурса. Для этого достаточно максимального из их концов. Именно максимум обнаружит конфликт, когда длинная запись охватывает несколько коротких.

Перенести максимум внутри ресурса

В примере первая бронь A длится до 14:00. Вторая заканчивается в 11:00, третья начинается в 12:00. Сравнение третьей только со второй ошибочно разрешит её. Накопленный максимум сохраняет 14:00, поэтому конфликт виден.

shift применяется отдельно по ресурсу: последнее окончание A не должно стать предыдущим окончанием B. Проверяем это на втором ресурсе в том же наборе.

import pandas as pd
x = pd.DataFrame({"resource": ["A", "A", "A", "B"], "booking": [1, 2, 3, 4],
 "start": ["10:00", "10:30", "12:00", "10:00"], "end": ["14:00", "11:00", "13:00", "11:00"]})
for c in ["start", "end"]:
    x[c] = pd.to_datetime("2026-10-01 " + x[c], format="%Y-%m-%d %H:%M")
assert x["end"].gt(x["start"]).all()
x = x.sort_values(["resource", "start", "booking"])
ends = x.groupby("resource")["end"].cummax()
x["previous_max_end"] = ends.groupby(x["resource"]).shift()
x["overlap"] = x["start"].lt(x["previous_max_end"])
print(x[["booking", "overlap"]].to_string(index=False))
assert x.loc[x["overlap"], "booking"].tolist() == [2, 3]

В очередь попали бронирования 2 и 3. Первая строка каждого ресурса не конфликтует с предыдущими, которых у неё нет. Алгоритм выявляет существование пересечения, но не перечисляет все пары конфликтующих бронирований. Если нужен список пар, потребуется отдельное сопоставление интервалов.

Выбрать действие после обнаружения

Для расписания пересечение обычно требует исправления источника. Для измерения занятости несколько одновременных записей могут быть законны: тогда нужно объединить интервалы и посчитать длину их объединения. Сумма исходных длительностей описывает объём записей, а не уникальное занятое время.

Сохраняйте booking, начало, конец и previous_max_end. По этим полям видно, почему запись отправилась в конфликт. Для конкретного предыдущего бронирования понадобится также идентификатор записи, установившей максимум.

Проверьте вложенный интервал, одинаковые начала, соседние интервалы и разные ресурсы. Если время имеет пояс, приведите его к согласованным моментам до сравнения. Календарные подписи сами по себе не гарантируют правильный порядок в неоднозначный местный час.

Закрепить на практике

В бесплатном уроке временных столбцов вы отработаете даты и длительности. После этой базы примените накопленный максимум из статьи к собственным интервалам и проверьте соседние границы. Откройте бесплатный урок и задачи. Полная последовательность тем — в бесплатном курсе.

Продолжить на практике

  • NumPy и pandas: практический тренажёр ↗

    Бесплатный тренажёр NumPy и pandas для тех, кто знает основы Python. Научитесь очищать таблицы, объединять данные и собирать отчёты: 31 урок и 124 задачи с кодом и автоматической проверкой.