Handling UTC and DST Edge Cases in Flight Time Math

The exact task this guide solves is computing durations — flight time, duty time, rest — that stay correct when the local clock does something the naive subtraction of two wall-clock times cannot survive: springs forward, falls back, or jumps a date line. A duty that reads ten hours on the departure station’s clock can be nine or eleven real hours if it straddles a daylight-saving transition, and a rest window that looks legal on local time can be an hour short. Every such bug over-credits or under-credits time in exactly the operations the regulation exists to protect. This page shows why UTC is the only defensible substrate, how to handle ambiguous and non-existent local times, and how to derive local time safely for the table lookups that genuinely need it. It underpins the flight time calculation algorithms and the temporal discipline the whole duty time validation rule engines domain depends on.

Prerequisites

The core rule: durations in UTC, wall time on demand

The one discipline that eliminates the entire class of DST bugs is to store every instant in UTC and compute every duration by subtracting UTC instants, deriving local wall-clock time only at the moment a rule needs it — for a Table B band lookup or an audit display. Subtracting two timezone-aware instants yields the true elapsed time regardless of what the local clock did in between, because the offset is baked into each instant. Local time is a view, never the substrate for arithmetic.

Step 1 — Compute duration from aware instants

The duration function takes two aware datetimes and returns elapsed minutes. It never touches a zone, because the instants already encode the absolute time.

from datetime import datetime, timezone


def elapsed_minutes(start: datetime, end: datetime) -> float:
    """True elapsed minutes between two timezone-aware instants."""
    if start.tzinfo is None or end.tzinfo is None:
        raise ValueError("both instants must be timezone-aware")
    return (end - start).total_seconds() / 60.0

Verify: a duty from 2026-03-08T06:30:00-05:00 (US spring-forward day) to 2026-03-08T17:30:00-04:00 returns 600.0 minutes — ten real hours — even though the local clock reads 06:30 to 17:30, an apparent eleven hours. The offset change from −05:00 to −04:00 is exactly the hour the naive local subtraction would have double-counted.

Step 2 — Resolve local time safely across the spring-forward gap

Some rules key on local time — Table B bands are in acclimatised local time — so local time must be derived, but derivation across a DST gap is where zoneinfo earns its place. In the spring-forward gap, a wall-clock time like 02:30 does not exist; zoneinfo resolves it using the fold attribute rather than raising, and the safe pattern is to always go through UTC.

from zoneinfo import ZoneInfo


def local_wall_time(instant_utc: datetime, zone: str) -> datetime:
    """Derive local wall-clock time from a UTC instant — always well-defined."""
    return instant_utc.astimezone(ZoneInfo(zone))

Verify: for 2026-03-08T07:30:00+00:00 in America/New_York, local_wall_time returns 02:30 folded correctly to the post-transition offset — deriving from UTC never lands in the non-existent gap, unlike constructing a local time directly.

Step 3 — Handle the fall-back ambiguous hour

In autumn, the fall-back hour repeats, so a local time like 01:30 occurs twice. If a feed delivers local times without offsets during that hour, the two occurrences are indistinguishable and the duration is ambiguous by an hour. The fold attribute disambiguates them, and the ingestion boundary must preserve it rather than discard it.

def disambiguate(local_naive: datetime, zone: str, *, second_occurrence: bool) -> datetime:
    """Attach a zone to a naive local time in the fall-back hour, choosing which pass."""
    tz = ZoneInfo(zone)
    return local_naive.replace(tzinfo=tz, fold=1 if second_occurrence else 0)

Verify: in America/New_York on the November fall-back date, 01:30 with fold=0 is the first (pre-transition, −04:00) pass and with fold=1 the second (post-transition, −05:00) pass; converting each to UTC yields instants one hour apart, so the correct fold decides the duration.

Spring-forward duty: UTC correct, local clock misleading A duty runs from 06:30 to 17:30 local time on a spring-forward day, apparently eleven hours on the wall clock. Because the clock jumped forward one hour during the duty, the true elapsed time in UTC is ten hours. The UTC track is drawn to scale as ten hours; the local track shows the one-hour spring-forward jump that a naive subtraction would miscount. UTC instants 11:30Z 21:30Z 10 h 00 m — true elapsed Local clock 06:30 −05:00 17:30 −04:00 02:00→03:00 springs forward wall clock reads 11 h — wrong
The same duty is ten hours in UTC and an apparent eleven on the local clock; the missing hour is the spring-forward jump, which only UTC arithmetic accounts for.

Failure modes and troubleshooting

Frequently Asked Questions

Why not just store local time and the offset?

Because arithmetic on local time is unsafe even with the offset attached if any code path subtracts the wall-clock components. Storing UTC as the substrate makes every duration correct by construction and reduces local time to a derived view for display and table lookups.

How do I handle a flight that departs before and lands after a DST change?

Compute its duration in UTC, which is unaffected by the transition, and derive the local report and arrival times separately from their UTC instants for any band lookup. The duration and the local times are resolved independently.

What is the fold attribute for?

It disambiguates the repeated hour at a fall-back transition, where the same wall-clock time occurs twice. fold=0 is the first occurrence, fold=1 the second; they convert to UTC instants one hour apart.

Does the date line need special handling?

Not for durations — UTC subtraction handles it. It matters for ordering and for rolling windows, which must be computed on UTC instants so a crossing does not reorder events, as covered in the rolling-window checks.

Back to Flight Time Calculation Algorithms.