2025, Dec 26 12:02
Корректная разница во времени в Polars: кейс с «24:00»
Разбираем, как в Polars корректно считать разницу времени, пересекающую полночь: парсинг «24:00», нормализация отрицательных интервалов и решение на выражениях
Вычисление разницы во времени, переходящей через полночь, в Polars кажется простым — пока в данных не встретится значение «24:00». Polars не признаёт «24:00» корректным литералом времени, поэтому прямое преобразование ломается. Если заменить «24:00» на «00:00», чтобы разбор прошёл, вычитание конца из начала может дать отрицательную длительность, которую придётся обрабатывать отдельно. Ниже — практический разбор проблемы и более аккуратное, общее решение.
Воспроизводим проблему
В наборе данных начало и конец представлены строками. В первом подходе мы преобразуем их во время, отдельно обрабатываем «24:00», а затем считаем разницу в часах, оперируя целочисленными представлениями вручную.
import polars as pl
data_frame = pl.DataFrame(
{
"begin": [
"23:00",
"00:00"
],
"finish": [
"24:00",
"01:00"
]
}
)
(
data_frame
.with_columns(
begin = pl.col("begin").str.to_time("%H:%M"),
finish = pl.col("finish").replace("24:00", "00:00").str.to_time("%H:%M")
)
.with_columns(
span = (
pl.when(pl.col("finish") == pl.time(0, 0, 0))
.then(86400000000000)
.otherwise(pl.col("finish").cast(pl.Int64))
- pl.col("begin").cast(pl.Int64)
) / 3600000000000
)
)
Что на самом деле происходит
Здесь две независимые сложности. Первая — разбор: Polars не умеет парсить «24:00», поэтому перед преобразованием это значение нужно заменить на «00:00». Вторая — арифметика: после разбора вычитание конца, перемотанного к «00:00», из позднего времени начала даёт отрицательную длительность. Это число действительно означает переход через полночь, но его нужно нормализовать до положительного промежутка в пределах суток. Исходный подход решает обе задачи, но делает это через перевод времени в числа и манипуляции с константами, из‑за чего логика становится менее прозрачной.
Более чистый и общий подход на выражениях
Более читаемая схема: посчитать исходную разницу выражением, при разборе заменить «24:00» на «00:00», а при отрицательном результате прибавить 24 часа. Так логика остаётся ровно такой, как мы её формулируем: вычисли дельту и, если она отрицательная, сдвинь на сутки вперёд.
import polars as pl
tbl = pl.DataFrame(
{"begin": ["23:00", "00:00", "23:30"],
"finish": ["24:00", "01:00", "00:35"]}
)
gap = pl.col("finish") - pl.col("begin")
result = (
tbl.with_columns(
begin=pl.col("begin").str.to_time("%H:%M"),
finish=pl.col("finish").replace("24:00", "00:00").str.to_time("%H:%M"),
)
.with_columns(
span=pl.when(gap < 0)
.then(pl.duration(hours=24) + gap)
.otherwise(gap)
)
)
print(result)
Результат показывает, что интервалы, пересекающие полночь, корректно нормализуются — в том числе пример, на котором ручной подход даёт сбой.
shape: (3, 3)
┌────────────┬──────────┬──────────────┐
│ start_time ┆ end_time ┆ duration │
│ --- ┆ --- ┆ --- │
│ time ┆ time ┆ duration[μs] │
╞════════════╪══════════╪══════════════╡
│ 23:00:00 ┆ 00:00:00 ┆ 1h │
│ 00:00:00 ┆ 01:00:00 ┆ 1h │
│ 23:30:00 ┆ 00:35:00 ┆ 1h 5m │
└────────────┴──────────┴──────────────┘
Почему это важно
Работа со временем, переходящим через полночь, часто подводит. Оформляя преобразование в виде понятного выражения Polars, мы явно фиксируем задумку, сохраняем декларативность запроса и не привязываем логику к низкоуровневым преобразованиям в целые числа и жёстко заданным константам. Замена «24:00» на «00:00» остаётся простым шагом парсинга, а правило нормализации отрицательных дельт аккуратно охватывает все ночные интервалы.
Выводы
Работая с длительностями через полночь в Polars, последовательно парсите время, считайте исходную разницу выражением и нормализуйте отрицательные интервалы, добавляя 24 часа. Такой подход читается естественно, масштабируется на большее число строк без лишних развилок и удерживает преобразование на уровне бизнес-логики, а не ручных пересчётов единиц.