Published on 4 min read

Check AI-Generated Calendar Times Before Creating Events

An AI assistant turns “Tuesday at nine in New York” into a calendar timestamp. It looks precise, yet the UTC offset may belong to winter rather than summer. Worse, a proposed time can fall inside an hour that never occurs when clocks move forward.

Before creating events, check that the proposed local time and offset agree with a user-confirmed time zone. This offline Python tutorial rejects inconsistent timestamps without contacting a calendar or calling a model. It complements AI-assisted weekly planning, rather than trusting the plan to validate itself.

A teal calendar and verification check connect to analog clocks showing different times against a warm background.

1. Set a narrow event contract

Use Python 3.9 or newer with IANA time-zone data available. Python’s zoneinfo documentation explains its system-data lookup and fallback to the first-party tzdata package.

Require a timestamp shaped exactly like 2025-07-15T09:00:00-04:00: full date, seconds, and numeric offset. This deliberately excludes abbreviations, missing offsets, fractional seconds, and Z; it is a small application contract, not a universal ISO 8601 validator. Validate JSON structure separately with the strict AI JSON checker.

Set America/New_York from reviewed user settings, not a model’s guess. Keep the original request and proposed timestamp for review. The fixtures below use historical 2025 dates, not actual bookings.

2. Convert to UTC, then back to the approved zone

Save this as check_calendar.py in a fresh directory:

import re
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

ZONE = ZoneInfo("America/New_York")
SHAPE = (r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"
         r"[+-](?:[01][0-9]|2[0-3]):[0-5][0-9]")
fixtures = [
    ("winter", "2025-01-15T09:00:00-05:00"),
    ("summer_wrong", "2025-07-15T09:00:00-05:00"),
    ("spring_gap", "2025-03-09T02:30:00-05:00"),
    ("fall_first", "2025-11-02T01:30:00-04:00"),
    ("fall_second", "2025-11-02T01:30:00-05:00"),
    ("no_offset", "2025-07-15T09:00:00"),
    ("bad_date", "2025-02-29T09:00:00-05:00"),
]


def check_stamp(value):
    if not isinstance(value, str) or not re.fullmatch(SHAPE, value):
        return "invalid format", None
    try:
        proposed = datetime.fromisoformat(value)
        instant = proposed.astimezone(timezone.utc)
        local = instant.astimezone(ZONE)
    except (ValueError, OverflowError):
        return "invalid datetime", None
    if (local.replace(tzinfo=None) != proposed.replace(tzinfo=None)
            or local.utcoffset() != proposed.utcoffset()):
        return "zone mismatch", None
    return "valid", instant


def main():
    accepted = 0
    for label, value in fixtures:
        reason, instant = check_stamp(value)
        suffix = " " + instant.isoformat() if instant is not None else ""
        print(f"{label}: {reason}{suffix}")
        accepted += reason == "valid"
    print(f"accepted={accepted}/{len(fixtures)}")
    return int(not fixtures or accepted != len(fixtures))


if __name__ == "__main__":
    raise SystemExit(main())

Python’s astimezone() documentation describes conversion that preserves the instant. Here, the supplied offset determines a UTC instant; converting back asks the approved zone what its local clock and offset actually were. Both must match the proposal. Comparing instants alone would miss a mislabeled local time. The format gate also bounds offset hours and minutes, preventing permissive parser normalization.

Do not validate by merely attaching ZoneInfo with replace(tzinfo=...): PEP 495 explains missing and repeated local times, and notes that constructors do not reject gap times. Removing tzinfo above only compares clock fields; it does not perform a conversion.

3. Verify rejection and corrected examples

Run:

python3 check_calendar.py

Expected stdout:

winter: valid 2025-01-15T14:00:00+00:00
summer_wrong: zone mismatch
spring_gap: zone mismatch
fall_first: valid 2025-11-02T05:30:00+00:00
fall_second: valid 2025-11-02T06:30:00+00:00
no_offset: invalid format
bad_date: invalid datetime
accepted=3/7

The command exits 1, with empty stderr: four records need review. zone mismatch intentionally covers both wrong offsets and nonexistent local times; it does not diagnose which correction the user intended. An empty batch also fails.

The two fall timestamps both pass, but represent instants one hour apart. An explicit offset resolves the repeated hour mechanically; it does not prove the user chose that occurrence. Test that distinction and two reviewed corrections:

python3 - <<'PYTEST'
import check_calendar as demo

first = demo.check_stamp("2025-11-02T01:30:00-04:00")[1]
second = demo.check_stamp("2025-11-02T01:30:00-05:00")[1]
assert (second - first).total_seconds() == 3600
assert demo.check_stamp(None)[0] == "invalid format"
assert demo.check_stamp("2025-01-15T09:00:00-04:60")[0] == "invalid format"
assert demo.check_stamp("2025-01-15T09:00:00+00:00")[0] == "zone mismatch"
print("edge checks passed")
demo.fixtures = [
    ("summer_fixed", "2025-07-15T09:00:00-04:00"),
    ("spring_fixed", "2025-03-09T03:30:00-04:00"),
]
raise SystemExit(demo.main())
PYTEST

Expected stdout:

edge checks passed
summer_fixed: valid 2025-07-15T13:00:00+00:00
spring_fixed: valid 2025-03-09T07:30:00+00:00
accepted=2/2

This exits 0, with empty stderr. Both commands were tested locally using Python 3.11 and installed tzdata 2023c. The second command changes only in-memory fixtures, leaving the saved script unchanged. Moving the spring appointment to 03:30 is an illustrative user-approved change, never an automatic repair.

4. Troubleshoot before enabling calendar writes

  • Missing zone data: ZoneInfoNotFoundError at startup means the approved zone cannot load. Provision system IANA data or the documented tzdata dependency; never fall back silently to the server’s zone.
  • Different results across machines: compare their time-zone data releases. IANA updates the database as governments change offsets and daylight-saving rules. Record your data version and update it before future bookings.
  • Mismatch: confirm the city, date, and intended clock time with the user. Do not repair every failure by changing the offset.
  • Valid but wrong meeting: this checks consistency only, not intent, attendee availability, duration, recurrence, or permission to write. Revalidate future occurrences individually under the relevant rules.

Use a read-only preview before granting calendar access, following the same separation of proposal and action as previewing AI file-renaming plans. Store the confirmed local timestamp, zone ID, and UTC instant together for auditing.

Practical takeaway

Require an explicit offset and a trusted zone, then verify their agreement before creating an event. A passing timestamp narrows the review; it never authorizes the booking by itself.

Related articles

Latest articles