Los dos silencios de la hora con huso
Una marca de tiempo con huso —2020-06-15T14:00:00+02:00— se vende como la forma portátil de fijar un instante. Lleva la fecha, la hora y el desplazamiento respecto a UTC, y con eso cualquiera puede recuperar el momento exacto. Es de las pocas cosas del oficio que casi todo el mundo da por resueltas.
Me puse a medir cuánto aguanta esa promesa hacia atrás, y salieron dos fallos distintos. Uno es ruidoso y ya lo esperaba. El otro es silencioso y no lo estaba buscando: lo encontré porque un caso de mi control se comportó raro.
Todos los números son de la tzdata 2026d instalada en la máquina donde medí, sobre las 598 zonas que expone zoneinfo. El código está al final.
Primer silencio: el formato tiene menos resolución que los datos
[RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) escribe el desplazamiento como ±HH:MM. Resolución: un minuto.
La base de datos de husos no guarda minutos. Para el tramo antiguo de casi cada zona guarda el LMT —hora media local—, que es la longitud del sitio dividida entre 15. Madrid: −00:14:44. Tokio antes de 1888: +09:18:59. Nueva York antes de 1883: −04:56:02. Números arbitrarios, con segundos, porque la longitud de una ciudad no es un múltiplo de quince grados.
Cuánto de la base está así:
- 505 de 598 zonas (84,4 %) tienen en su historia al menos un desplazamiento que no es múltiplo de 60 s. - De los 406 desplazamientos distintos que han estado vigentes alguna vez, el 86 % no es múltiplo de 15 minutos y el 79 % no es múltiplo de un minuto.
Y el borde no está donde uno lo imagina. Yo había apostado por el año 1972 y salió 1972, pero el detalle importa más que el año:
> El último desplazamiento con segundos vigente en tzdata es el de Africa/Monrovia, > −00:44:30, que termina el 7 de enero de 1972.
Es decir: dos años dentro del tiempo Unix. En el instante t = 0 —1970-01-01T00:00:00Z— hay exactamente una zona del mundo cuyo desplazamiento no se puede escribir en RFC 3339. Liberia. El epoch de Unix nace con un país que no cabe en el formato.
Qué hacen los analizadores reales
Pasé el mismo sello por diez implementaciones. Las cadenas las genera datetime.isoformat() de Python, que es lo que de verdad sale de un servicio: Python emite ±HH:MM:SS cuando hace falta, cosa que RFC 3339 no contempla.
Con un desplazamiento con segundos (1969-12-31T23:15:30-00:44:30):
| Analizador | Resultado | |---|---| | Python fromisoformat | correcto | | Python strptime %z | correcto | | dateutil.isoparse | rechaza | | pandas Timestamp | rechaza | | Node new Date() | rechaza | | Go time.RFC3339 | rechaza | | sqlite3 strftime | rechaza | | GNU date -d | rechaza |
Los mismos ocho aciertan con error cero en la batería moderna (seis sellos con +00:00, +02:00, −04:00, +05:30, +05:45), y los diez rechazan las tres cadenas basura que les metí de control. Así que el aparato sabe decir «sí» y sabe decir «no».
Yo había apostado —con confianza 0,60— a que al menos uno aceptaría la cadena y devolvería un instante equivocado, truncando los segundos en silencio. Perdí la apuesta, y me alegro de perderla:
> Cero truncadores. De ocho a cero. Ninguno miente. Todos rompen.
Eso es una buena noticia y hay que decirla como tal. El fallo es real —Python serializa por defecto algo que seis de sus seis vecinos no saben leer— pero es un fallo ruidoso, que te estalla en la cara en el primer test en vez de envenenarte los datos durante dos años.
Segundo silencio: el pasado que devuelve no es el del sitio que pediste
Uno de mis casos de tratamiento era Europe/Amsterdam en 1900. Debía salir un desplazamiento con segundos. Salió +00:00.
Europe/Amsterdam es un enlace a Europe/Brussels: mismo fichero binario, mismo para 1900, la historia belga.
Lo que hacían los holandeses en 1900 era otra cosa: la hora media de Ámsterdam, +0:19:32,13, el meridiano de la Westertoren, práctica común durante décadas y [ley desde mayo de 1909](https://en.wikipedia.org/wiki/Time_in_the_Netherlands), vigente hasta julio de 1937. Casi veinte minutos. La API no lo menciona.
Quise saber cuánta superficie tiene esto y me hizo falta una regla que no dependiera de ningún historiador. La hay, y es geometría: el LMT de un sitio es su longitud partida por 15, y zone.tab trae las coordenadas de cada nombre de zona. Si el LMT guardado en el fichero de un nombre no coincide con el que le tocaría por su propia longitud, ese nombre está sirviendo el pasado de otro sitio.
El suelo del instrumento salió perfecto: entre las zonas canónicas, la mediana del desacuerdo es 0,0 segundos exactos (percentil 90: 11,3 s). El LMT de tzdata se deriva de esas mismas coordenadas, así que donde no hay fusión no hay discrepancia. Un control que da cero exacto vale mucho.
Sobre los 418 nombres con coordenadas, restringido a donde la hora solar media significa algo (|latitud| ≤ 60; volveré sobre esto):
- 86 nombres discrepan más de 5 minutos. De ellos, 84 son enlaces (97,7 %), y los 84 apuntan a una zona de otro país. - Mediana del desacuerdo sobre los 86: 17,6 minutos. Entre los 84 enlaces, el mayor es 60,8 minutos (Bangui). Los dos que no son enlaces son Pacific/Kanton y Antarctica/Macquarie, islas donde el punto de zone.tab no es donde se puso el reloj.
Algunos, para que se vea la forma:
| Pides | Te contesta | Desacuerdo | |---|---|---| | Africa/Bangui (RCA) | Africa/Lagos | 60,8 min | | Africa/Dakar (Senegal) | Africa/Abidjan | 53,6 min | | America/Atikokan (Canadá) | America/Panama | 48,4 min | | Africa/Kinshasa (RDC) | Africa/Lagos | 47,6 min | | Indian/Antananarivo (Madagascar) | Africa/Nairobi | 42,8 min | | Europe/Ljubljana (Eslovenia) | Europe/Belgrade | 23,9 min |
Y uno que queda fuera del recorte de latitud pero merece mencionarse aparte, porque el enlace es real y el número también: Atlantic/Reykjavik apunta a Africa/Abidjan, a 71,3 minutos. La historia anterior a 1970 de Islandia se sirve desde Abiyán.
Y aquí está lo importante: no es un fallo
La política está escrita. El documento [theory.html](https://data.iana.org/time-zones/theory.html) del propio proyecto dice de los datos anteriores a 1970 que «cubren sólo una astilla de cómo se comportaron realmente los relojes» y que «la mayoría vienen de fuentes poco fiables, a menudo libros de astrología sin citas cuyos compiladores evidentemente se inventaban entradas cuando los hechos verdaderos no se sabían». Las zonas que coinciden desde 1970 se fusionan; lo descartado se recoge en un fichero backzone que se distribuye aparte y que, en la máquina donde medí, no está compilado: por eso Ámsterdam es Bruselas en el binario.
O sea que la base de datos avisa. Quien no avisa es la interfaz. ZoneInfo(nombre) contesta con exactamente el mismo aplomo para 1900 que para 2020. No hay un tipo distinto, ni una bandera, ni una advertencia. El dato antiguo y el moderno salen por el mismo agujero y con la misma cara.
Y ése es el contraste que me llevo del día:
> El formato falla a gritos: rompe en ocho analizadores de ocho. > Los datos fallan en silencio: 84 nombres te devuelven el pasado de otro país sin > decir una palabra. > > Fui a buscar al mentiroso a la capa del formato, donde no estaba, y me lo encontré una > capa más abajo porque un control se comportó raro.
Si trabajas con fechas anteriores a 1970
Tres cosas prácticas, que es a lo que iba todo esto:
1. No transportes el desplazamiento. Guarda UTC más el nombre de la zona. El desplazamiento con segundos no cabe en RFC 3339 y isoformat() de Python te lo va a emitir igual: es una bomba de interoperabilidad que no estalla hasta el otro extremo del cable. 2. Comprueba si tu nombre de zona es un enlace antes de fiarte de una respuesta anterior a 1970. grep -E "^L .+ Europe/Amsterdam$" /usr/share/zoneinfo/tzdata.zi tarda un segundo. 253 de los nombres son enlaces. 3. Para historia de verdad, tzdata no es la fuente. El propio proyecto lo dice. Si necesitas 1900, necesitas backzone compilado a mano, o una fuente histórica, y en cualquier caso saber que estás fuera del alcance para el que la base se mantiene.
Lo que no medí, y en qué me equivoqué
diez aciertos, siete fallos y una anulada (era condicional a que hubiera truncadores, y no los hubo). Las que fallé dicen más:
- Aposté a que ≥90 % de las zonas tendrían algún desplazamiento sub-minuto: salió 84,4 %. Fallo limpio. - Aposté a que ≥85 % de las zonas darían segundos para un instante de 1900: salió 58,7 %. Fallo gordo, y ahora sé por qué: buena parte de esas zonas ya no tienen su propio tramo LMT, precisamente porque están fusionadas. Los dos silencios estaban interfiriendo en mi propia medida sin que yo lo supiera. - Aposté a que habría truncadores silenciosos. Cero. Ya está contado arriba. - Mi control positivo falló, y fue lo mejor que pasó en el día: dos de mis diez analizadores (R con as.POSIXct y perl con Time::Piece) rechazaban todo, incluidos los sellos modernos. No eran hallazgos sobre R ni sobre perl: era mi arnés roto. Los saqué de la cuenta. Sin el control positivo, los habría publicado como dos fallos más y habría quedado más redondo. - Mi primera versión de la regla del LMT tenía dos errores que el discriminador línea de fecha daban 1440 minutos exactos, que no es un desacuerdo sino la misma hora escrita desde el otro lado) y leía mal el tipo inicial de algunos ficheros binarios. Reparé, volví a sellar y volví a medir. discordantes eran enlaces, no el 90 % que había pedido. Los catorce que sobraban resultaron ser la Antártida y el Ártico canadiense —sitios donde «hora media solar» no quiere decir nada: las bases antárticas van a la hora de su país de suministro y las zonas árticas cubren decenas de grados—. Recortar a |latitud| ≤ 60 sube la fracción a 97,7 %, pero eso es post-hoc y así lo etiqueto: es el dominio donde mi regla vale, descubierto después de que la regla se rompiera fuera de él, no una apuesta ganada.
Nada de esto mide cuánta gente guarda fechas anteriores a 1970, porque no tengo ese dato. Y los números son de tzdata 2026d: otra versión puede moverlos.
Y una nota sobre dos aciertos: las dos apuestas del apéndice que gané (que habría más de 40 nombres discordantes y que el peor pasaría de media hora) las gané con el instrumento roto. Sobreviven a la reparación —104 y 686 minutos— pero las magnitudes que vi primero (121 y 1466) estaban infladas por el artefacto. Un acierto con un aparato malo no es un acierto: es suerte con buena letra.
Medido el 23 de septiembre de 2026 sobre tzdata 2026d. Las cuatro herramientas datos, que es la única forma que conozco de que «me lo esperaba» signifique algo.