Publicado el 5 min de lectura

Comprueba las horas de calendario de IA antes de crear eventos

En esta página

Un asistente de IA convierte «el martes a las nueve en Nueva York» en una marca de tiempo para el calendario. Parece precisa, pero su desfase respecto a UTC puede corresponder al invierno y no al verano. Peor aún, la hora propuesta puede caer dentro de una franja que no existe cuando se adelantan los relojes.

Antes de crear eventos, comprueba que la hora local y el desfase propuestos concuerdan con una zona horaria confirmada por el usuario. Este tutorial de Python funciona sin conexión y rechaza marcas de tiempo incoherentes sin contactar con un calendario ni llamar a un modelo. Complementa la planificación semanal con IA, en lugar de confiar en que el plan se valide solo.

Un calendario verde azulado y una marca de verificación conectados con relojes analógicos que muestran horas distintas sobre un fondo cálido.

1. Define un contrato limitado para el evento

Usa Python 3.9 o posterior con datos de zonas horarias de IANA disponibles. La documentación de zoneinfo explica la búsqueda de datos del sistema y el uso alternativo del paquete oficial tzdata.

Exige una marca de tiempo con el formato exacto 2025-07-15T09:00:00-04:00: fecha completa, segundos y desfase numérico. Se excluyen deliberadamente abreviaturas, desfases ausentes, fracciones de segundo y Z; es un contrato pequeño para una aplicación, no un validador universal de ISO 8601. Valida la estructura JSON por separado con el comprobador estricto de JSON de IA.

Establece America/New_York a partir de preferencias revisadas por el usuario, no de una suposición del modelo. Conserva la petición original y la marca de tiempo propuesta para revisarlas. Los casos de prueba siguientes usan fechas históricas de 2025, no reservas reales.

2. Convierte a UTC y vuelve a la zona aprobada

Guarda lo siguiente como check_calendar.py en un directorio nuevo:

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())

La documentación de astimezone() describe una conversión que conserva el instante. Aquí, el desfase proporcionado determina un instante UTC; al convertirlo de vuelta, se consulta qué hora local y desfase correspondían realmente a la zona aprobada. Ambos deben coincidir con la propuesta. Comparar solo los instantes no detectaría una hora local mal etiquetada. El filtro de formato también limita las horas y los minutos del desfase para evitar normalizaciones permisivas del analizador.

No valides limitándote a asignar ZoneInfo mediante replace(tzinfo=...): PEP 495 explica las horas locales inexistentes y repetidas, y señala que los constructores no rechazan las horas del salto. Quitar tzinfo en el código anterior solo sirve para comparar los campos del reloj; no realiza una conversión.

3. Verifica los rechazos y los ejemplos corregidos

Ejecuta:

python3 check_calendar.py

Salida estándar esperada:

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

El comando termina con código 1 y sin salida de error: cuatro registros necesitan revisión. zone mismatch abarca deliberadamente tanto desfases incorrectos como horas locales inexistentes; no diagnostica qué corrección pretendía el usuario. Un lote vacío también falla.

Las dos marcas de tiempo de otoño pasan, pero representan instantes separados por una hora. Un desfase explícito resuelve mecánicamente la hora repetida; no demuestra que el usuario eligiera esa aparición. Comprueba esa diferencia y dos correcciones revisadas:

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

Salida estándar esperada:

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

El comando termina con código 0 y sin salida de error. Ambos comandos se probaron localmente con Python 3.11 y los datos tzdata 2023c instalados. El segundo solo cambia los casos de prueba en memoria y no modifica el archivo guardado. Mover la cita de primavera a las 03:30 ilustra un cambio aprobado por el usuario, nunca una reparación automática.

4. Resuelve los problemas antes de permitir escrituras en el calendario

  • Faltan datos de zona: ZoneInfoNotFoundError al iniciar significa que no se puede cargar la zona aprobada. Proporciona datos IANA del sistema o la dependencia tzdata documentada; no uses silenciosamente la zona del servidor.
  • Resultados distintos entre equipos: compara sus versiones de datos de zonas horarias. IANA actualiza la base de datos cuando los gobiernos cambian los desfases y las reglas de horario de verano. Registra la versión utilizada y actualízala antes de reservar fechas futuras.
  • Incoherencia: confirma con el usuario la ciudad, la fecha y la hora local deseada. No intentes reparar todos los fallos cambiando el desfase.
  • Marca válida, reunión equivocada: esto solo comprueba la coherencia, no la intención, disponibilidad de asistentes, duración, recurrencia ni permiso de escritura. Vuelve a validar cada repetición futura con las reglas correspondientes.

Usa una vista previa de solo lectura antes de conceder acceso al calendario, separando propuesta y acción igual que al revisar planes de renombrado de archivos de IA. Guarda juntos la marca local confirmada, el identificador de zona y el instante UTC para facilitar la auditoría.

Conclusión práctica

Exige un desfase explícito y una zona de confianza, y comprueba que concuerdan antes de crear el evento. Una marca de tiempo válida reduce el trabajo de revisión; nunca autoriza por sí sola la reserva.

Artículos relacionados

Últimos artículos