whenever ZonedDateTime: DST gaps, folds and calendar math: working versions (tested 2026-10-18)
Timezone-safe datetime arithmetic with whenever's ZonedDateTime: resolving DST gaps and folds, converting to Instant, and telling exact durations apart from calendar units.
passed6 passed · 0 failed2026-10-18 03:57 UTC- Status
- passed
- Last passed
- 2026-10-18 03:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
whenever@0.11.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("whenever-zoned-datetime-arithmetic") (install), or see them on GitHub.
requirements.txt
whenever==0.11.0 pytest==9.1.1
scheduling.py
"""DST-aware scheduling helpers built on whenever 0.11."""
import whenever as w
def wall_clock(text: str, tz: str, disambiguation: str = "raise") -> w.ZonedDateTime:
"""Resolve a local wall-clock time, refusing to guess in DST gaps/folds."""
return w.ZonedDateTime.parse_iso(f"{text}[{tz}]", disambiguation=disambiguation)
def exact_deadline(start: w.ZonedDateTime, *, hours: int = 0, minutes: int = 0) -> w.Instant:
"""A fixed amount of elapsed time later, immune to DST."""
return start.add(hours=hours, minutes=minutes).to_instant()
def calendar_deadline(start: w.ZonedDateTime, *, days: int = 0, months: int = 0) -> w.ZonedDateTime:
"""The same wall-clock time later, so DST shifts the underlying instant."""
return start.add(days=days, months=months, disambiguation="compatible")
def elapsed_hours(a: w.ZonedDateTime, b: w.ZonedDateTime) -> float:
return (b.to_instant() - a.to_instant()).total("hours")
def calendar_months(a: w.ZonedDateTime, b: w.ZonedDateTime) -> int:
return int(b.since(a, total="months"))
tests/test_scheduling.py
import pytest
import whenever as w
from scheduling import (
calendar_deadline,
calendar_months,
elapsed_hours,
exact_deadline,
wall_clock,
)
def test_fall_back_fold_needs_disambiguation():
earlier = wall_clock("2024-11-03T01:30", "America/New_York", disambiguation="earlier")
later = wall_clock("2024-11-03T01:30", "America/New_York", disambiguation="later")
assert earlier.offset == w.hours(-4) and later.offset == w.hours(-5)
assert earlier.is_repeated() and later.is_repeated()
assert elapsed_hours(earlier, later) == 1.0
with pytest.raises(w.RepeatedTime):
wall_clock("2024-11-03T01:30", "America/New_York")
def test_spring_forward_gap_shifts_instant():
with pytest.raises(w.SkippedTime):
wall_clock("2024-03-10T02:30", "America/New_York")
compatible = wall_clock("2024-03-10T02:30", "America/New_York", disambiguation="compatible")
assert compatible.hour == 3 and compatible.minute == 30
assert compatible.to_instant() == w.Instant.from_utc(2024, 3, 10, 7, 30)
def test_calendar_day_versus_exact_24_hours():
start = w.ZonedDateTime(2024, 3, 9, 12, tz="America/New_York")
calendar = calendar_deadline(start, days=1)
exact = exact_deadline(start, hours=24)
assert calendar.hour == 12
assert exact.to_tz("America/New_York").hour == 13
assert exact.timestamp() - calendar.to_instant().timestamp() == 3600
def test_month_addition_clamps_to_end_of_month():
assert wall_clock("2024-01-31T12:00", "UTC").add(months=1).day == 29
assert wall_clock("2023-01-31T12:00", "UTC").add(months=1).day == 28
def test_calendar_span_differs_from_exact_span():
start = w.ZonedDateTime(2024, 3, 1, 12, tz="America/New_York")
end = calendar_deadline(start, months=1)
assert calendar_months(start, end) == 1
assert end.since(start, total="days") == 31.0
assert elapsed_hours(start, end) == 743.0
def test_instant_round_trip_is_zone_agnostic():
instant = w.Instant.from_utc(2024, 3, 10, 7, 0)
ny = instant.to_tz("America/New_York")
utc = instant.to_tz("UTC")
assert ny.format_iso() != utc.format_iso()
assert ny == utc and ny.to_instant() == instant
assert ny.to_instant().to_tz("Asia/Tokyo") == instant
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.