Fechas en texto: límites entre hora local y UTC
Una sonda sobre cadenas de fecha: una fecha sin hora se lee como medianoche UTC mientras que una fecha con hora sin desfase se lee como local, y un día inválido se arrastra.
BotBrowser Team
Quieres la documentación estructurada de Huella digital?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
BotBrowser permite reproducir un fixture del mismo origen autorizado bajo un perfil declarado y guardar un registro acotado de la marca de tiempo exacta que produjo una página, pero no puede hacer que una cadena de solo fecha y una cadena de fecha y hora nombren el mismo instante, no puede impedir que un día fuera de rango se arrastre, ni puede hacer que una cadena guardada se ordene como se ordena su instante. Esta revisión ejecuta una sonda sobre diez casos fijos en un host con America/Toronto y registra qué formas de cadena lee Date.parse como UTC y cuáles como hora local, cómo cuatro formas que deletrean el mismo día calendario se vuelven tres instantes distintos, qué hace un día fuera de rango y un hueco de horario de verano, por qué un orden por cadena guardada y un orden por instante discrepan, y a qué hora local cae cada forma.
Resumen
- Una cadena de solo fecha se lee como medianoche UTC, así que la sonda imprime
"2026-10-31" -> 2026-10-31T00:00:00.000Zyutc-equal -> Date.UTC=1793404800000 parse=1793404800000. - Una fecha con hora sin desfase se lee como hora local, así que
"2026-10-31T00:00:00"se vuelve2026-10-31T04:00:00.000Zen un host a UTC-4. - Cuatro formas que deletrean el mismo día producen
distinct -> 3 instants from 4 shapes, porque la forma de solo fecha es UTC y las formas con desfase mueven el instante. - Un día fuera de rango se arrastra: la sonda pide el 30 de febrero y obtiene el 2 de marzo, y
"2026-02-30"se analiza como 2 de marzo, no como error. - Una hora de pared local dentro de un hueco de horario de verano se mueve hacia adelante, así que
02:30pasa a03:30local, mientras que un pliegue conserva una de las dos apariciones de01:30. - Un orden por la cadena guardada y un orden por el instante discrepan: la sonda imprime
sort-by-ms -> 2026-10-31 , 2026-10-30T23:00:00 , ...frente asort-by-string -> 2026-10-30T23:00:00 , 2026-10-31 , .... Una segunda comparación que expone la sonda es que dos cadenas pueden nombrar el mismo instante y aun así ordenarse distinto, el mismo desajuste que orden de array y comparadores cubre para un comparador que no es un orden total consistente. La forma conZse lee igual en toda zona, mientras que la forma con hora se desplaza por el desfase del host y la forma de solo fecha es estable pero cae a otra hora local.
Contenido
- Qué formas se leen como UTC y cuáles como locales
- Por qué cuatro escrituras de un día son tres instantes
- Qué se arrastra y qué falla
- Por qué un orden por cadena y un orden por instante discrepan
- Registrar una lectura de análisis de fechas
Qué formas se leen como UTC y cuáles como locales
Los mismos campos de calendario pueden nombrar varios instantes distintos, y cuál elige una forma depende de la forma y no de la fecha. La sonda analiza cuatro cadenas e imprime el instante ISO y el tiempo numérico que produjo cada una:
"2026-10-31" -> 2026-10-31T00:00:00.000Z ms=1793404800000
"2026-10-31T00:00:00" -> 2026-10-31T04:00:00.000Z ms=1793419200000
"2026-10-31T00:00:00Z" -> 2026-10-31T00:00:00.000Z ms=1793404800000
"2026-10-31 00:00:00" -> 2026-10-31T04:00:00.000Z ms=1793419200000
distinct -> 3 instants from 4 shapes
shape:2026-10-31 -> 2026-10-31T00:00:00.000Z
shape:2026-10-31T00:00:00 -> 2026-10-31T04:00:00.000Z
shape:2026-10-31T00:00:00-04:00 -> 2026-10-31T04:00:00.000Z
shape:2026-10-31T00:00:00+09:00 -> 2026-10-30T15:00:00.000Z
utc-equal -> Date.UTC=1793404800000 parse=1793404800000
La primera línea fija la regla para una fecha desnuda. "2026-10-31" se analiza como 2026-10-31T00:00:00.000Z, y la última línea lo confirma: utc-equal muestra que el valor analizado iguala exactamente a Date.UTC(2026, 9, 31). Una cadena de solo fecha no tiene por tanto lectura local alguna; es medianoche UTC por definición, y una página que la formatea de vuelta con un formateador local en un host a UTC-4 mostrará 30 de octubre, 20:00, que es el día anterior para un lector que esperaba el 31.
La segunda línea fija la regla para una fecha con hora sin desfase. "2026-10-31T00:00:00" se analiza como 04:00Z en este host, porque el desfase ausente se rellena con la zona del host, que es UTC-4 en esa fecha. El valor no es por tanto el mismo instante que la cadena de solo fecha, y una página que mezcla las dos formas en un mismo conjunto de datos tiene dos puntos de inicio distintos para lo que parece un solo día.
La tercera línea es la forma que no tiene ambigüedad. "2026-10-31T00:00:00Z" se analiza como 00:00Z, y lo hará en cualquier host, porque la Z final nombra UTC explícitamente. Un desfase numérico explícito hace lo mismo: la forma -04:00 de las líneas shape: imprime 04:00Z, y la forma +09:00 imprime el día anterior a 15:00Z, ambas el mismo instante sin importar dónde corra el código.
La cuarta línea es la riesgosa. "2026-10-31 00:00:00" usa un espacio en lugar de una T, y la sonda muestra que se analiza como 04:00Z, la misma lectura local que obtuvo la forma con T. Eso hace que la forma con espacio funcione igual en este host, pero la forma con espacio está definida por la implementación y no es obligatoria, así que un tiempo de ejecución puede leerla distinto o rechazarla. Las únicas formas que una aplicación debería almacenar son la de solo fecha, que es UTC, y una forma de fecha con hora con Z o un desfase explícito. Cómo un instante guardado se vuelve una fecha visible es un paso aparte, y formato de fecha y hora y portabilidad de zona horaria registra qué hace un formateador con la zona que recibe.
Por qué cuatro escrituras de un día son tres instantes
La línea distinct -> 3 instants from 4 shapes es el resumen del problema. Cuatro cadenas que un lector llamaría «31 de octubre» se vuelven tres instantes distintos, y la razón es que dos de las cuatro nombran el mismo instante por dos caminos diferentes.
La forma de solo fecha es 00:00Z. La forma sin desfase es 04:00Z en este host, cuatro horas más tarde. La forma -04:00 también es 04:00Z, así que coincide con la forma sin desfase por una coincidencia de la zona de este host y no por una regla. La forma +09:00 es 15:00Z del día anterior, cinco horas antes que el par 04:00Z. Así que las cuatro cadenas abarcan desde el día anterior hasta el día pedido, y una página que las trata como intercambiables ordenará dos mal y colocará otras dos en el día anterior.
La coincidencia entre la forma sin desfase y la forma -04:00 es la trampa. En un host a UTC-4 imprimen el mismo instante, así que una prueba que corre solo en ese host pasa, y el mismo código en un host a UTC+9 imprime dos instantes distintos para las dos formas. Una prueba que quiera demostrar que el código es independiente de la zona tiene que correr la comparación en al menos dos zonas, o tiene que afirmar contra formas con Z y con desfase que no dependen del host en absoluto.
El instante que nombra una cadena guardada es por tanto una propiedad de la forma de la cadena y, para las formas sin marcar, de la zona del lector. Por eso un registro tiene que guardar el instante como número o cadena Z en lugar de como hora de pared local, y por eso una visualización que muestra una fecha debe formatear desde el instante con una zona nombrada en lugar de releer la hora de pared que la cadena implicaba.
La línea offset-now -> 240 de la misma sonda hace el mismo punto desde el otro lado. La fecha 2026-10-31 está en horario de verano en este host, así que su desfase es -4. Un host que la formateara con un desfase fijo -05:00 de invierno estaría una hora desviado para esa fecha y correcto en invierno, que es el fallo que un desfase fijo escrito en un fixture suele esconder hasta que la fecha cruza una transición.
Las dos formas restantes de la sonda son las que suele producir un filtro de panel. "2026" se analiza como 2026-01-01T00:00:00.000Z y "2026-10" como 2026-10-01T00:00:00.000Z, ambas como UTC porque una fecha ISO parcial no tiene hora ni desfase. Un filtro que envía un mes como 2026-10 nombra por tanto el primer instante de ese mes en UTC, que es el mes anterior para un lector al oeste de Greenwich: en este host la lectura local es 2026-09-30T20:00, una etiqueta de mes que muestra el mes equivocado. Un panel que quiere «octubre» para un usuario de Toronto tiene que enviar un inicio explícito como 2026-10-01T00:00:00-04:00 en lugar de un mes desnudo, o enviar el mes como texto y formatearlo sin convertirlo en un instante.
Las formas "10/31/2026" y "Oct 31 2026" se comportan al revés, y muestran por qué una cadena formateada por el idioma nunca debe ser el formato de almacenamiento. Ambas se analizan como 2026-10-31T04:00:00.000Z aquí, una lectura local, así que siguen la zona del host como la forma sin desfase. Pero su aceptación está definida por la implementación, así que un tiempo de ejecución que sigue otro orden de fecha puede leer 10/31/2026 como el décimo del trigésimo primer mes, o rechazarla. Una página que guarda una fecha en el formato que imprime su idioma y luego la analiza en un servidor en otro idioma tiene una forma que se acepta en un lugar y se lee distinto en otro, y el fallo aparece como un mes equivocado en lugar de como un error.
La línea de ida y vuelta responde si una hora de pared local sobrevive a guardar y leer. La sonda analiza 2026-10-31T09:30:00, imprime el instante 2026-10-31T13:30:00.000Z, relee localHours=9, y confirma que el número guardado devuelve el mismo instante. Eso es un ida y vuelta correcto de una hora de pared en un host, y es exactamente la forma que se rompe en otro host: la misma cadena leída en una zona a UTC+1 cae en 08:30Z, así que el instante guardado se mueve cinco horas y la hora de pared mostrada sigue siendo 09:30 solo porque la visualización usa la misma zona que la analizó. Una página que guarda la hora de pared y la formatea de vuelta en la misma zona no ve error, y el error aparece solo cuando los dos extremos corren en zonas distintas.
Qué se arrastra y qué falla
No toda fecha inválida falla, y la sonda separa las formas que se arrastran de las que lanzan. Construye tres fechas imposibles o límite:
rollover -> 2026-03-02T05:00:00.000Z (Feb 30 requested)
rollover-leap -> 2026-03-01T05:00:00.000Z (Feb 29 in a common year)
parse-invalid -> "2026-02-30" -> 2026-03-02T00:00:00.000Z
El constructor Date arrastra un día fuera de rango al mes siguiente. new Date(2026, 1, 30) pide el 30 de febrero, que no existe en un año común, y el resultado es el 2 de marzo, porque el constructor suma los días de más al último día válido de febrero. Lo mismo vale para el 29 de febrero en un año no bisiesto: new Date(2026, 1, 29) se vuelve 1 de marzo. Así que una página que construye una fecha desde tres números analizados obtiene un arrastre silencioso, y el getMonth() del resultado no es el mes que se pasó.
El análisis de la forma de cadena hace lo mismo. new Date('2026-02-30') devuelve 2026-03-02T00:00:00.000Z en lugar de Invalid, porque la cadena se lee como una fecha de solo fecha y el día se arrastra. Eso es lo contrario de lo que muchos validadores suponen, y significa que una comprobación escrita como if (isNaN(Date.parse(value))) deja pasar una cadena que ningún calendario contiene. Una validación que deba rechazar el 30 de febrero tiene que comparar los campos analizados con los campos de entrada en lugar de confiar en la aceptación del analizador.
Los casos de horario de verano muestran el mismo ajuste silencioso para horas de pared. La línea del hueco pide 02:30 el día del salto de primavera de EE. UU., una hora que no existe, y la sonda imprime 2026-03-08T07:30:00.000Z con la hora local leyendo 03:30. El tiempo de ejecución movió la hora de pared inexistente hacia adelante por el hueco, así que la fecha construida es una hora más tarde de la pedida y el getHours() del resultado es 3, no 2. La línea del pliegue pide 01:30 el día del retroceso de otoño, una hora que ocurre dos veces, y la sonda imprime 2026-11-01T05:30:00.000Z con la lectura local 01:30, que es una de las dos apariciones y no necesariamente la que un lector quiso.
Una página que programa una tarea a una hora de pared local hereda por tanto los dos bordes. En primavera puede empezar una hora tarde porque la hora pedida no existía, y en otoño puede empezar en la primera de dos apariciones, lo que para una tarea que corre una vez está bien y para una que cuenta apariciones no. El registro de tal tarea tiene que guardar el instante que se eligió, no la hora de pared que pidió, porque la hora de pared no identifica la ejecución de forma única.
Por qué un orden por cadena y un orden por instante discrepan
Los dos órdenes de la misma lista son la consecuencia más clara de las reglas de análisis, porque discrepan sobre el orden de las mismas cuatro cadenas. La sonda ordena una lista por Date.parse y otra vez por comparación de cadenas simple:
sort-by-ms -> 2026-10-31 , 2026-10-30T23:00:00 , 2026-10-31T00:00:00 , 2026-11-01
sort-by-string -> 2026-10-30T23:00:00 , 2026-10-31 , 2026-10-31T00:00:00 , 2026-11-01
Ordenar por instante pone 2026-10-31 primero, porque es medianoche UTC de ese día, y pone 2026-10-30T23:00:00 segundo, porque esa es la hora de pared local del día anterior leída como 03:00Z del día siguiente. Ordenar por cadena pone 2026-10-30T23:00:00 primero, porque los caracteres 2026-10-30 ordenan antes de 2026-10-31. Los dos órdenes discrepan por tanto en los dos primeros elementos, y la discrepancia viene por completo de las formas y no de los valores.
Un comparador que no es un orden total consistente da un orden erróneo pero silencioso, el mismo fallo que documenta orden de array y comparadores. El orden de array por defecto es el orden de cadena, porque compara las unidades de código de los elementos. Una lista de fechas ordenada con el sort() por defecto está por tanto ordenada por su texto, que coincide con el orden cronológico solo cuando cada elemento usa la misma forma. Mezcla un valor de solo fecha con un valor de fecha y hora y el orden de texto y el orden cronológico divergen, y nada avisa de que lo hicieron.
La zona que nombra una cadena serializada es también la que una guía de zona horaria e idioma tiene que resolver antes de poder etiquetar una fecha. El arreglo es ordenar por el número analizado y guardar una sola forma. La línea sort-by-ms de la sonda es ese arreglo, y ordena las mismas cuatro cadenas por el instante que nombran. Una página que guarda cada marca de tiempo como cadena Z o número y ordena por ese valor obtiene el mismo orden de un orden por cadena y de un orden numérico, porque la forma es uniforme; una página que mezcla formas obtiene dos órdenes distintos y no puede decir cuál verá un lector.
Las líneas local-hour muestran el mismo problema desde el lado de la visualización. local-hour:2026-10-31 -> 20 lee la medianoche UTC del valor de solo fecha en la zona del host y reporta 20:00 del día anterior, mientras que local-hour:2026-10-31T00:00:00 -> 0 lee la forma local y reporta 00:00, y local-hour:2026-10-31T00:00:00Z -> 20 lee la forma con Z y reporta el mismo 20:00 que el valor de solo fecha. Así que dos de las tres formas se muestran como la tarde anterior, y solo la forma local se muestra como la medianoche pedida. Un registro que guarda la hora mostrada sin la forma no puede explicar por qué dos páginas que dicen «31 de octubre» muestran horas distintas.
Las líneas compare y diff-hours cierran el círculo. La sonda imprime compare -> '2026-10-31' == new Date('2026-10-31') is true, que solo dice que la cadena analizada iguala a sí misma, y luego imprime diff-hours -> dateOnlyUtcHour=0 dateTimeUtcHour=4, que muestra que las dos formas caen cuatro horas aparte en UTC en este host. Una comparación entre un valor de solo fecha y un valor de fecha y hora no es por tanto una comparación del mismo día; es una comparación de un instante a 00:00Z contra un instante a 04:00Z en este host, que difieren aunque ambas cadenas empiecen con la misma fecha calendario.
Registrar una lectura de análisis de fechas
Una lectura de análisis de fechas sostiene una afirmación acotada: este tiempo de ejecución, esta zona, estas formas, produjeron estos instantes. Cuatro campos la mantienen verificable:
- La forma, porque un valor de solo fecha es UTC y una fecha con hora sin desfase es local, y la forma no se ve en el instante.
- La zona del host, porque la forma sin desfase depende de ella mientras que las formas con
Zy con desfase no, y una lectura sin la zona no se puede reproducir. - El instante en UTC, porque la hora de pared que ve un lector es un segundo paso que puede diferir por zona.
- La clave de orden y la forma guardada, porque un orden por cadena y un orden por instante de una lista mixta discrepan, y un registro debe decir cuál produjo el orden.
Fuentes públicas
- MDN: Date
- MDN: Date.parse()
- MDN: Date.UTC()
- ECMAScript: formato de cadena de fecha y hora
- MDN: Intl.DateTimeFormat
- IANA: base de datos tz
- BotBrowser: referencia de consistencia de automatización
Conclusión
Guarda una sola forma y guárdala como instante: una cadena Z o un número, nunca una hora de pared local, porque un valor de solo fecha y un valor de fecha con hora sin desfase nombran instantes distintos y el segundo depende de la zona del lector. No confíes en que Date.parse rechace un día imposible, ya que el 30 de febrero se arrastra al 2 de marzo en lugar de devolver Invalid; comprueba los campos analizados contra la entrada. Ordena por el número analizado en lugar de por la comparación de cadenas por defecto cuando una lista pueda tener más de una forma, y recuerda que una hora de pared dentro de un hueco de horario de verano se mueve hacia adelante mientras que una dentro de un pliegue es una de dos apariciones. Para la mitad de visualización del mismo problema, lee formato de fecha y hora y portabilidad de zona horaria; para los datos de idioma que nombran una zona, lee zonas horarias e idioma.
Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?
Artículos Relacionados
Formato de fechas y portabilidad de zonas horarias
Huella digitalICU: alias de zona horaria y la TZ que informas
Huella digitalOrden de array y comparadores: una función fácil de errar
Huella digitalUnicode: cuatro identidades de texto que lee una página
Huella digitalAnálisis de URL y search params: qué cambia el viaje
Huella digitalIntl.Collator: el orden local no es el de código
Lleva BotBrowser de la investigación a producción
Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.