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 часа. Такой подход читается естественно, масштабируется на большее число строк без лишних развилок и удерживает преобразование на уровне бизнес-логики, а не ручных пересчётов единиц.