Para convertir una fecha de publicación y una zona horaria con nombre en un instante exacto de medianoche, resuelve 00:00:00 local según las reglas de esa zona, comprueba que exista y luego conviértela a UTC. Este procedimiento en Terminal de Mac también detecta las medianoches repetidas.

Supongamos que un registro de publicación sintético contiene la fecha 2025-07-15 y la zona America/New_York. Tu tarea es calcular la marca de tiempo que representa la medianoche al comienzo de esa fecha. El procedimiento siguiente genera una marca de tiempo que puedes copiar en una herramienta que acepte UTC, y mantiene visibles la fecha y la zona originales para que puedas verificarlas.

¿Qué información define un instante de medianoche?

Mantén separados estos cuatro valores durante todo el cálculo:

Valor Ejemplo sintético Significado
Fecha civil 2025-07-15 Una fecha del calendario gregoriano sin hora ni zona
Fecha y hora locales 2025-07-15T00:00:00 La lectura de reloj solicitada
Zona horaria con nombre America/New_York Las reglas que se usan para interpretar esa lectura local
Instante resuelto 2025-07-15T04:00:00Z Un momento específico expresado en UTC

Python representa una fecha de calendario con date, combina los campos de fecha y hora con datetime y usa la información de zona horaria para interpretar un objeto datetime con zona. Su clase timezone representa desplazamientos fijos; zoneinfo proporciona reglas de zonas con nombre. Consulta la documentación oficial de datetime y la documentación de zoneinfo.

Un desplazamiento como -04:00 es la diferencia entre la hora local y UTC para una lectura concreta. Una zona con nombre proporciona reglas que pueden producir distintos desplazamientos en fechas diferentes. RFC 9557 explica por qué sustituir una zona con nombre por un desplazamiento fijo puede dar lugar a cálculos posteriores incorrectos. Conserva el nombre de la zona en vez de copiar un desplazamiento de otro registro de publicación. RFC 9557: Fecha y hora en Internet: marcas de tiempo con información adicional

Para esta tarea, «medianoche» significa exactamente 00:00:00 al comienzo de la fecha indicada. Deja claro este requisito antes de calcular nada: una solicitud para obtener el primer momento válido de un día calendario requiere una política distinta cuando no hay medianoche.

¿Por qué hay que comprobar la medianoche antes de la conversión?

Los cambios del reloj local pueden crear un intervalo inexistente, en el que una lectura del reloj no ocurre, o un intervalo repetido, en el que ocurre dos veces. La PEP 495 de Python define estos casos y proporciona el atributo fold para distinguir las lecturas repetidas. También documenta que los constructores y replace() no rechazan automáticamente las horas locales inexistentes. Por tanto, asignar una zona no valida por sí solo la medianoche solicitada. PEP 495: desambiguación de la hora local

Usa la siguiente regla de decisión para un requisito de publicación de medianoche exacta:

Instantes de medianoche válidos encontrados Interpretación Acción
Uno La medianoche es única Devuelve ese instante
Dos La medianoche se repite Rechaza hasta que se elija una política de ocurrencia anterior o posterior
Ninguno No existe la medianoche exacta Informa que no se puede cumplir el requisito con esas entradas

El programa siguiente implementa esta regla probando ambos valores de fold, convirtiendo cada candidato a UTC y convirtiéndolo de vuelta a la zona con nombre. Un candidato solo se acepta si los campos locales devueltos coinciden exactamente con la medianoche solicitada. Los resultados UTC duplicados se reducen a un único candidato. Esta secuencia de validación se basa en el comportamiento documentado de conversión y fold; es lógica adicional de la aplicación. datetime.astimezone(), PEP 495

¿Cómo se ejecuta el cálculo en Terminal de Mac?

Abre Spotlight, escribe Terminal y abre la aplicación. También puedes abrir /Applications/Utilities en Finder y hacer doble clic en Terminal. Estas son las formas documentadas por Apple para abrir una ventana de comandos. Abrir o salir de Terminal en Mac

Ejecuta el siguiente comando para comprobar tu versión de Python:

python3 --version

Este ejemplo requiere Python 3.9 o posterior, versión en la que pasó a estar disponible zoneinfo. Si Python no está instalado o es una versión anterior, sigue la guía oficial de instalación para macOS para obtener un instalador de Python actualmente compatible desde Python.org y repite la comprobación de versión. El procedimiento usa módulos de la biblioteca estándar. zoneinfo: compatibilidad con zonas horarias IANA, Uso de Python en macOS

Copia todo el bloque siguiente en Terminal y pulsa Return. Es un comando completo que contiene el programa de Python. Para calcular otra marca de tiempo de publicación, edita DATE_TEXT y ZONE_NAME antes de pegarlo. Deja REPEATED como reject, a menos que hayas elegido deliberadamente una política para las medianoches repetidas.

python3 - <<'PY'
from datetime import date, datetime, time, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

DATE_TEXT = '2025-07-15'
ZONE_NAME = 'America/New_York'
REPEATED = 'reject'  # 'reject', 'earlier', or 'later'


def resolve_midnight(date_text, zone_name, repeated='reject'):
    if repeated not in {'reject', 'earlier', 'later'}:
        raise ValueError('Repeated-time policy must be reject, earlier, or later.')

    day = date.fromisoformat(date_text)
    if day.isoformat() != date_text:
        raise ValueError('Use a date in YYYY-MM-DD format.')

    zone = ZoneInfo(zone_name)
    wall = datetime.combine(day, time.min)
    candidates = set()

    for fold in (0, 1):
        guess = wall.replace(tzinfo=zone, fold=fold)
        instant = guess.astimezone(timezone.utc)
        back = instant.astimezone(zone)
        if back.replace(tzinfo=None) == wall:
            candidates.add(instant)

    ordered = sorted(candidates)
    if not ordered:
        raise ValueError('Exact midnight does not exist on this date in this zone.')

    if len(ordered) == 2 and repeated == 'reject':
        choices = ', '.join(x.isoformat() for x in ordered)
        raise ValueError(
            'Midnight repeats. Choose earlier or later. UTC choices: ' + choices
        )

    chosen = ordered[-1] if repeated == 'later' else ordered[0]
    return chosen.astimezone(zone), chosen


try:
    local, utc = resolve_midnight(DATE_TEXT, ZONE_NAME, REPEATED)
except ZoneInfoNotFoundError:
    raise SystemExit(
        'Zone not found. Check the IANA name and time zone data installation.'
    )
except (ValueError, OverflowError) as exc:
    raise SystemExit(str(exc))

print('Civil date:', DATE_TEXT)
print('Named zone:', ZONE_NAME)
print('Local midnight:', local.isoformat())
print('UTC instant:', utc.isoformat().replace('+00:00', 'Z'))
PY

Para el ejemplo proporcionado, el resultado es:

Fecha civil: 2025-07-15
Zona con nombre: America/New_York
Medianoche local: 2025-07-15T00:00:00-04:00
Instante UTC: 2025-07-15T04:00:00Z

Lee juntas las dos líneas de marcas de tiempo. La línea local confirma que la hora de reloj solicitada es medianoche. La línea UTC identifica el mismo instante: al restar el desplazamiento local de cuatro horas negativas se obtiene 04:00:00 UTC. RFC 3339 define una marca de tiempo con una fecha, una hora y un desplazamiento numérico o Z. RFC 3339: Fecha y hora en Internet: marcas de tiempo

¿Cómo se comprueban los resultados habituales y excepcionales?

Las siguientes entradas son ejemplos sintéticos de publicación elegidos para poner a prueba el programa. Sus resultados se calcularon con el resolutor anterior usando la base de datos de zonas horarias disponible; no son registros de publicaciones reales. Cambia los dos valores de entrada y vuelve a ejecutar el comando completo para cada caso.

Fecha Zona con nombre Resultado del resolutor
2025-01-15 America/New_York Un instante: 2025-01-15T05:00:00Z
2025-07-15 America/New_York Un instante: 2025-07-15T04:00:00Z
2018-11-04 America/Sao_Paulo No existe la medianoche exacta; se rechaza
2020-11-01 America/Havana La medianoche se repite; se rechaza de forma predeterminada
2011-12-30 Pacific/Apia No existe la medianoche exacta; se rechaza

Las dos primeras filas demuestran por qué la fecha forma parte del cálculo: en la misma zona con nombre, la medianoche se resuelve a horas UTC distintas. Para el ejemplo de medianoche repetida en La Habana, establecer REPEATED = 'earlier' selecciona 2020-11-01T04:00:00Z; establecerlo en later selecciona 2020-11-01T05:00:00Z. Los nombres se refieren al orden cronológico en la línea de tiempo UTC.

Un resultado de medianoche inexistente requiere decidir qué exige el requisito de publicación. Si la medianoche exacta es esencial, elige explícitamente otra fecha o zona. Si el requisito previsto es la primera hora válida de esa fecha, define e implementa esa operación por separado, incluido qué hacer cuando el día entero no existe. Este resolutor informa del fallo deliberadamente en lugar de sustituir silenciosamente la hora local por otra.

¿Qué debes hacer cuando falla una entrada?

Si hay un error de fecha, proporciona una fecha gregoriana real en formato YYYY-MM-DD, como 2025-07-15. El programa comprueba tanto la validez del calendario como ese formato exacto. Una entrada como 2025-02-30 falla en vez de pasar al mes siguiente.

Si aparece Zone not found, comprueba que el identificador IANA esté completo, incluida la ortografía y las mayúsculas. Python documenta que esta excepción puede indicar una clave no válida o la ausencia de datos de zonas horarias. La búsqueda estándar usa primero los datos del sistema y, como alternativa, el paquete de origen tzdata. Si faltan los datos, instala ese paquete en el entorno de Python que estés usando y vuelve a ejecutar el resolutor. PEP 615: compatibilidad con la base de datos de zonas horarias IANA en la biblioteca estándar

Mantén juntos en tus notas de trabajo la fecha original, el nombre de la zona, la opción elegida para la hora repetida y la marca de tiempo UTC resuelta. Si los cálculos difieren entre computadoras, compara sus fuentes de datos de zonas horarias. Instalar un paquete alternativo no reemplaza automáticamente los datos del sistema que Python ya encuentra. Fuentes de datos de zoneinfo

¿Qué valor debes copiar en otra herramienta?

Cuando el destino acepte una marca de tiempo UTC, copia el valor de Instante UTC, incluido su Z. Comprueba el formato de entrada documentado por el destino antes de introducirlo. Un campo que solo admita una fecha no puede conservar toda la información representada por esa marca de tiempo resuelta; conserva la fecha original y la zona con nombre junto a ella si necesitas mantener la intención de programación.

RFC 9557 define un formato de marca de tiempo extendido que puede incluir un sufijo de zona con nombre, pero ese formato sigue representando un instante resuelto. También reconoce que las actualizaciones de los datos de zonas pueden crear inconsistencias entre los desplazamientos almacenados y la información de la zona. Usa una representación extendida solo si la herramienta receptora documenta que la admite. RFC 9557: alcance y desplazamientos incoherentes

Termina comprobando que el resultado local muestre la fecha prevista a las 00:00:00, que la zona con nombre coincida con tu entrada y que hayas elegido deliberadamente cualquier opción de medianoche repetida. Una vez superadas esas comprobaciones, el resultado UTC es el instante de medianoche resuelto según las reglas usadas en ese cálculo.

Fuentes