¿Por qué Date.parse da resultados incorrectos?

344

Caso uno:

new Date(Date.parse("Jul 8, 2005"));

Salida:

Vie 08 de julio 2005 00:00:00 GMT-0700 (PST)

Caso dos:

new Date(Date.parse("2005-07-08"));

Salida:

Jue 07 jul 2005 17:00:00 GMT-0700 (PST)


¿Por qué es incorrecto el segundo análisis?

usuario121196
fuente
30
El segundo análisis no es incorrecto per se, es solo que el primero se analiza en hora local y el segundo en UTC. Tenga en cuenta que "Jue 07/07 2005 17:00:00 GMT-0700 (PST)" es lo mismo que "2005-07-08 00:00".
jches
18
ISO 8601 xkcd.
ulidtko
1
En caso de que alguien viniera aquí para descubrir por qué una fecha está regresando NaNen Firefox, descubrí que la mayoría de los otros navegadores (y Node.js) analizarán una fecha sin un día, como "abril de 2014" como el 1 de abril de 2014, pero Firefox devuelve NaN. Debes pasar una fecha adecuada.
Jazzy
Para agregar al comentario anterior de Jason: si está recibiendo un NaN en Firefox, otro problema podría ser que a Firefox y Safari no les gustan las fechas con guiones. Solo Chrome lo hace. Use una barra en su lugar.
Bangkokiano

Respuestas:

451

Hasta que salió la especificación de la quinta edición, el Date.parsemétodo era completamente dependiente de la implementación ( new Date(string)es equivalente a Date.parse(string)excepto que este último devuelve un número en lugar de un a Date). En la especificación de la quinta edición, el requisito se agregó para admitir un ISO-8601 simplificado (y ligeramente incorrecto) (consulte también ¿Qué son las cadenas de fecha y hora válidas en JavaScript? ). Pero aparte de eso, no había ninguna necesidad de lo que Date.parse/ new Date(string)debe aceptar distinta de la que tenían que aceptar lo que Date#toStringla salida (sin decir lo que era).

A partir de ECMAScript 2017 (edición 8), se requirió que las implementaciones analizaran su salida para Date#toStringy Date#toUTCString, pero no se especificó el formato de esas cadenas.

A partir de ECMAScript 2019 (edición 9), el formato para Date#toStringy Date#toUTCString, se ha especificado como (respectivamente):

  1. ddd MMM DD AAAA HH: mm: ss ZZ [(nombre de la zona horaria)]
    por ejemplo, martes 10 de julio de 2018 18:39:58 GMT + 0530 (IST)
  2. ddd, DD MMM AAAA HH: mm: ss Z
    ej. Martes 10 jul 2018 13:09:58 GMT

proporcionando 2 formatos más que Date.parsedeberían analizarse de manera confiable en nuevas implementaciones (señalando que el soporte no es ubicuo y que las implementaciones no conformes seguirán en uso durante algún tiempo).

Recomendaría que las cadenas de fecha se analicen manualmente y el constructor de fecha se use con argumentos de año, mes y día para evitar la ambigüedad:

// parse a date in yyyy-mm-dd format
function parseDate(input) {

  let parts = input.split('-');

  // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
  return new Date(parts[0], parts[1]-1, parts[2]); // Note: months are 0-based
}
CMS
fuente
Excelente, tuve que usar esto ya que Date.parseno me estaba comportando con los formatos de fecha del Reino Unido por alguna razón que no pude resolver
Ben
1
Las partes de tiempo están documentadas en el código @CMS. Utilicé este código con un formato de fecha de "2012-01-31 12:00:00" return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);Funciona perfectamente, ¡gracias!
Richard Rout
1
@CMS, ¿qué quiere decir con implementación dependiente ?
Royi Namir
3
@RoyiNamir, significa que los resultados dependen de qué navegador web (u otra implementación de JavaScript) esté ejecutando su código.
Samuel Edwin Ward,
1
También he tenido problemas con la nueva Fecha (cadena) en diferentes navegadores que se comportan de manera diferente. Ni siquiera se trata de que se rompa en versiones antiguas de IE, los diferentes navegadores simplemente no son consistentes. No utilice Date.parse o una nueva Fecha (cadena) nunca.
Hoffmann
195

Durante la experiencia reciente escribiendo un intérprete de JS luché mucho con el funcionamiento interno de las fechas de ECMA / JS. Entonces, supongo que arrojaré mis 2 centavos aquí. Espero que compartir estas cosas ayude a otros con cualquier pregunta sobre las diferencias entre los navegadores en cómo manejan las fechas.

El lado de entrada

Todas las implementaciones almacenan sus valores de fecha internamente como números de 64 bits que representan la cantidad de milisegundos (ms) desde 1970-01-01 UTC (GMT es lo mismo que UTC). Esta fecha es la época de ECMAScript que también utilizan otros lenguajes como los sistemas Java y POSIX como UNIX. Las fechas que ocurren después de la época son números positivos y las fechas anteriores son negativas.

El siguiente código se interpreta como la misma fecha en todos los navegadores actuales, pero con el desplazamiento de la zona horaria local:

Date.parse('1/1/1970'); // 1 January, 1970

En mi zona horaria (EST, que es -05: 00), el resultado es 18000000 porque esa es la cantidad de ms en 5 horas (son solo 4 horas durante los meses de verano). El valor será diferente en diferentes zonas horarias. Este comportamiento se especifica en ECMA-262, por lo que todos los navegadores lo hacen de la misma manera.

Si bien existe una cierta variación en los formatos de cadena de entrada que los principales navegadores analizarán como fechas, esencialmente los interpretan de la misma manera en lo que respecta a las zonas horarias y al horario de verano, aunque el análisis depende en gran medida de la implementación.

Sin embargo, el formato ISO 8601 es diferente. Es uno de los dos únicos formatos descritos en ECMAScript 2015 (ed. 6) específicamente que todas las implementaciones deben analizar de la misma manera (el otro es el formato especificado para Date.prototype.toString ).

Pero, incluso para las cadenas de formato ISO 8601, algunas implementaciones se equivocan. Aquí hay una salida de comparación de Chrome y Firefox cuando esta respuesta se escribió originalmente para el 1/1/1970 (la época) en mi máquina usando cadenas de formato ISO 8601 que deberían analizarse exactamente con el mismo valor en todas las implementaciones:

Date.parse('1970-01-01T00:00:00Z');       // Chrome: 0         FF: 0
Date.parse('1970-01-01T00:00:00-0500');   // Chrome: 18000000  FF: 18000000
Date.parse('1970-01-01T00:00:00');        // Chrome: 0         FF: 18000000
  • En el primer caso, el especificador "Z" indica que la entrada está en tiempo UTC, por lo que no está desplazada de la época y el resultado es 0
  • En el segundo caso, el especificador "-0500" indica que la entrada está en GMT-05: 00 y ambos navegadores interpretan que la entrada está en la zona horaria -05: 00. Eso significa que el valor UTC se compensa de la época, lo que significa agregar 18000000ms al valor de hora interna de la fecha.
  • El tercer caso, donde no hay especificador, debe tratarse como local para el sistema host. FF trata correctamente la entrada como hora local, mientras que Chrome la trata como UTC, por lo que produce diferentes valores de hora. Para mí, esto crea una diferencia de 5 horas en el valor almacenado, lo cual es problemático. Otros sistemas con diferentes compensaciones obtendrán diferentes resultados.

Esta diferencia se ha solucionado a partir de 2020, pero existen otras peculiaridades entre los navegadores al analizar cadenas de formato ISO 8601.

Pero se pone peor. Una peculiaridad de ECMA-262 es que se requiere que el formato de fecha ISO 8601 (AAAA-MM-DD) se analice como UTC, mientras que ISO 8601 requiere que se analice como local. Aquí está la salida de FF con los formatos de fecha ISO largos y cortos sin especificador de zona horaria.

Date.parse('1970-01-01T00:00:00');       // 18000000
Date.parse('1970-01-01');                // 0

Entonces, el primero se analiza como local porque es la fecha y hora ISO 8601 sin zona horaria, y el segundo se analiza como UTC porque es solo la fecha ISO 8601.

Entonces, para responder la pregunta original directamente, "YYYY-MM-DD" ECMA-262 requiere que se interprete como UTC, mientras que el otro se interpreta como local. Es por eso:

Esto no produce resultados equivalentes:

console.log(new Date(Date.parse("Jul 8, 2005")).toString()); // Local
console.log(new Date(Date.parse("2005-07-08")).toString());  // UTC

Esto hace:

console.log(new Date(Date.parse("Jul 8, 2005")).toString());
console.log(new Date(Date.parse("2005-07-08T00:00:00")).toString());

La conclusión es esto para analizar cadenas de fechas. La ÚNICA cadena ISO 8601 que puede analizar de forma segura en los navegadores es la forma larga con un desplazamiento (ya sea ± HH: mm o "Z"). Si lo hace, puede ir y venir con seguridad entre la hora local y la hora UTC.

Esto funciona en todos los navegadores (después de IE9):

console.log(new Date(Date.parse("2005-07-08T00:00:00Z")).toString());

La mayoría de los navegadores actuales tratan los otros formatos de entrada por igual, incluidos los '1/1/1970' (M / D / AAAA) y '1/1/1970 00:00:00 AM' (M / D / AAAA hh) utilizados con frecuencia. : mm: ss ap) formatos. Todos los siguientes formatos (excepto el último) se tratan como entrada de hora local en todos los navegadores. La salida de este código es la misma en todos los navegadores en mi zona horaria. El último se trata como -05: 00 independientemente de la zona horaria del host porque el desplazamiento se establece en la marca de tiempo:

console.log(Date.parse("1/1/1970"));
console.log(Date.parse("1/1/1970 12:00:00 AM"));
console.log(Date.parse("Thu Jan 01 1970"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00 GMT-0500"));

Sin embargo, dado que el análisis de incluso los formatos especificados en ECMA-262 no es coherente, se recomienda no confiar nunca en el analizador incorporado y analizar siempre las cadenas manualmente, por ejemplo, utilizando una biblioteca y proporcionar el formato al analizador.

Por ejemplo, en moment.js podrías escribir:

let m = moment('1/1/1970', 'M/D/YYYY'); 

El lado de salida

En el lado de salida, todos los navegadores traducen las zonas horarias de la misma manera, pero manejan los formatos de cadena de manera diferente. Aquí están las toStringfunciones y lo que generan. Observe la salida toUTCStringy las toISOStringfunciones a las 5:00 AM en mi máquina. Además, el nombre de la zona horaria puede ser una abreviatura y puede ser diferente en diferentes implementaciones.

Convierte de UTC a hora local antes de imprimir

 - toString
 - toDateString
 - toTimeString
 - toLocaleString
 - toLocaleDateString
 - toLocaleTimeString

Imprime la hora UTC almacenada directamente

 - toUTCString
 - toISOString 

En cromo
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-05:00 (Eastern Standard Time)
toLocaleString      1/1/1970 12:00:00 AM
toLocaleDateString  1/1/1970
toLocaleTimeString  00:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

En Firefox
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-0500 (Eastern Standard Time)
toLocaleString      Thursday, January 01, 1970 12:00:00 AM
toLocaleDateString  Thursday, January 01, 1970
toLocaleTimeString  12:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

Normalmente no uso el formato ISO para la entrada de cadenas. El único momento en que usar ese formato es beneficioso para mí es cuando las fechas deben clasificarse como cadenas. El formato ISO se puede ordenar tal cual, mientras que los demás no. Si tiene que tener compatibilidad entre navegadores, especifique la zona horaria o use un formato de cadena compatible.

El código new Date('12/4/2013').toString()pasa por la siguiente pseudo-transformación interna:

  "12/4/2013" -> toUCT -> [storage] -> toLocal -> print "12/4/2013"

Espero que esta respuesta haya sido útil.

drankin2112
fuente
3
En primer lugar, esto es un relato fantástico. Sin embargo, quería señalar una dependencia. Con respecto a los especificadores de zona horaria, usted declaró: "La ausencia de un especificador debe suponer una entrada de hora local". Afortunadamente, el estándar ECMA-262 elimina cualquier necesidad de presumir. Establece : "El valor de un desplazamiento de zona horaria ausente es" Z "". Por lo tanto, se supone que una cadena de fecha / hora sin una zona horaria especificada es una UTC en lugar de una hora local. Por supuesto, como con tantas cosas JavaScript, parece haber poco acuerdo entre las implementaciones.
Daniel
2
... incluidos los formatos '1/1/1970' y '1/1/1970 00:00:00 AM' más utilizados. - ¿ Dónde se usa con más frecuencia ? Eso no está en mi país, seguro.
ulidtko
2
@ulidtko - Lo siento, estoy en los Estados Unidos. Wow ... estás justo allí en Kiev. Espero que usted y su familia se mantengan a salvo y que las cosas se estabilicen pronto. Cuídate y buena suerte con todo.
drankin2112
Solo una nota aquí. Parece que esto no funciona para los navegadores Safari (es decir, iOS u OSX). Eso o tengo algún otro problema.
keyneom
1
@ Daniel: afortunadamente, los autores de ECMAScript corrigieron su error con respecto a la zona horaria faltante para las representaciones de fecha y hora. Ahora las cadenas de fecha y hora sin una zona horaria utilizan el desplazamiento de la zona horaria del host (es decir, "local"). Confusamente, los formularios con fecha ISO 8601 solo se tratan como UTC (aunque no está particularmente claro en las especificaciones), mientras que ISO 8601 los trata como locales, por lo que no lo arreglaron todo.
RobG
70

Hay algún método para la locura. Como regla general, si un navegador puede interpretar una fecha como ISO-8601, lo hará. "2005-07-08" cae en este campamento, por lo que se analiza como UTC. "8 de julio de 2005" no puede, por lo que se analiza en la hora local.

Ver JavaScript y fechas, ¡qué desastre! para más.

danvk
fuente
3
" Como regla general, si un navegador puede interpretar una fecha como un ISO-8601, lo hará " no es compatible. "2020-03-20 13:30:30" es tratado como ISO 8601 y local por muchos navegadores, pero no es válido por Safari. Hay muchos formatos ISO 8601 que no son compatibles con la mayoría de los navegadores, por ejemplo, 2004-W53-7 y 2020-092.
RobG
7

Otra solución es construir una matriz asociativa con formato de fecha y luego reformatear los datos.

Este método es útil para la fecha formateada de manera no usual.

Un ejemplo:

    mydate='01.02.12 10:20:43':
    myformat='dd/mm/yy HH:MM:ss';


    dtsplit=mydate.split(/[\/ .:]/);
    dfsplit=myformat.split(/[\/ .:]/);

    // creates assoc array for date
    df = new Array();
    for(dc=0;dc<6;dc++) {
            df[dfsplit[dc]]=dtsplit[dc];
            }

    // uses assc array for standard mysql format
    dstring[r] = '20'+df['yy']+'-'+df['mm']+'-'+df['dd'];
    dstring[r] += ' '+df['HH']+':'+df['MM']+':'+df['ss'];
guido rizzi
fuente
5

Use moment.js para analizar fechas:

var caseOne = moment("Jul 8, 2005", "MMM D, YYYY", true).toDate();
var caseTwo = moment("2005-07-08", "YYYY-MM-DD", true).toDate();

El tercer argumento determina el análisis estricto (disponible a partir de 2.3.0). Sin él, moment.js también puede dar resultados incorrectos.

Lukasz Wiktor
fuente
4

De acuerdo con http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html, el formato "aaaa / mm / dd" resuelve los problemas habituales. Él dice: "Adhiérase a" AAAA / MM / DD "para sus cadenas de fecha siempre que sea posible. Es universalmente compatible y sin ambigüedades. Con este formato, todos los horarios son locales". He establecido pruebas: http://jsfiddle.net/jlanus/ND2Qg/432/ Este formato: + evita la ambigüedad del pedido de día y mes mediante el uso de pedidos ymd y un año de 4 dígitos + evita el problema UTC vs. local no cumplir con el formato ISO mediante el uso de barras inclinadas + danvk, el tipo de gráficos , dice que este formato es bueno en todos los navegadores.

Juan Lanus
fuente
Es posible que desee ver la respuesta del autor .
Brad Koch
Yo diría que la solución con el ejemplo en jsFiddle funciona bastante bien si usa jQuery ya que usa el analizador datepicker. En mi caso, el problema es con jqGrid, pero descubrí que tiene su método parseDate. Pero de todos modos, el ejemplo me ayudó al darme una idea, así que +1, gracias.
Vasil Popov
2
Ese artículo sobre gráficos es incorrecto y el primer ejemplo en la página ilustra claramente por qué usar barras en lugar de guiones es realmente un mal consejo. En el momento en que se escribió el artículo, el uso de "13/03/2012" dio como resultado que el navegador lo analizara como una fecha local, en lugar de UTC. La especificación ECMAScript define el soporte explícito para usar "AAAA-MM-DD" (ISO8601), por lo que siempre use guiones. Cabe señalar que en el momento en que escribo este comentario, Chrome ha sido parcheado para tratar las barras como UTC.
Lachlan Hunt
4

Si bien CMS tiene razón en que pasar cadenas al método de análisis generalmente no es seguro, la nueva especificación ECMA-262 5th Edition (también conocida como ES5) en la sección 15.9.4.2 sugiere que en Date.parse()realidad debería manejar las fechas con formato ISO. La antigua especificación no hizo tal afirmación. Por supuesto, los navegadores antiguos y algunos navegadores actuales aún no ofrecen esta funcionalidad ES5.

Tu segundo ejemplo no está mal. Es la fecha especificada en UTC, según lo implica Date.prototype.toISOString(), pero está representada en su zona horaria local.

peller
fuente
1
Y el análisis de las cadenas de fecha se modificó nuevamente en ECMAScript 2015 para que "2005-07-08" sea local, no UTC. Por cierto, ES5 no era el estándar hasta junio de 2011 (y actualmente lo es ECMAScript 2015). ;-)
RobG
1
Solo para confundir las cosas, TC39 decidió en octubre (solo un mes después de mi publicación anterior) que "2005-07-08" debería ser UTC , sin embargo, "" 2005-07-08T00: 00: 00 "debería ser local. Ambos son ISO 8601 formatos compatibles, ambos no tienen una zona horaria, pero se tratan de manera diferente. Ir a la figura.
RobG
2

Esta biblioteca de análisis de fechas de peso ligero debería resolver todos los problemas similares. Me gusta la biblioteca porque es bastante fácil de extender. También es posible hacerlo (no muy sencillo, pero no tan difícil).

Ejemplo de análisis:

var caseOne = Date.parseDate("Jul 8, 2005", "M d, Y");
var caseTwo = Date.parseDate("2005-07-08", "Y-m-d");

Y formateando de nuevo a cadena (notará que ambos casos dan exactamente el mismo resultado):

console.log( caseOne.dateFormat("M d, Y") );
console.log( caseTwo.dateFormat("M d, Y") );
console.log( caseOne.dateFormat("Y-m-d") );
console.log( caseTwo.dateFormat("Y-m-d") );
Nux
fuente
2

Aquí hay un fragmento corto y flexible para convertir una cadena de fecha y hora de una manera segura para todos los navegadores, como detalla nicel por @ drankin2112.

var inputTimestamp = "2014-04-29 13:00:15"; //example

var partsTimestamp = inputTimestamp.split(/[ \/:-]/g);
if(partsTimestamp.length < 6) {
    partsTimestamp = partsTimestamp.concat(['00', '00', '00'].slice(0, 6 - partsTimestamp.length));
}
//if your string-format is something like '7/02/2014'...
//use: var tstring = partsTimestamp.slice(0, 3).reverse().join('-');
var tstring = partsTimestamp.slice(0, 3).join('-');
tstring += 'T' + partsTimestamp.slice(3).join(':') + 'Z'; //configure as needed
var timestamp = Date.parse(tstring);

Su navegador debe proporcionar el mismo resultado de marca de tiempo que Date.parsecon:

(new Date(tstring)).getTime()
Lorenz Lo Sauer
fuente
Sugeriría agregar T a la expresión regular para capturar fechas ya formateadas en JS también: inputTimestamp.split (/ [T \ /: -] / g)
andig
Si divide la cadena en partes componentes, el siguiente paso más confiable es usar esas partes como argumentos para el constructor Fecha. Crear otra cadena para darle al analizador solo lo lleva de vuelta al paso 1. "2014-04-29 13:00:15" debe analizarse como local, su código lo formatea de nuevo como UTC. :-(
RobG
2

Ambos son correctos, pero se interpretan como fechas con dos zonas horarias diferentes. Entonces comparaste manzanas y naranjas:

// local dates
new Date("Jul 8, 2005").toISOString()            // "2005-07-08T07:00:00.000Z"
new Date("2005-07-08T00:00-07:00").toISOString() // "2005-07-08T07:00:00.000Z"
// UTC dates
new Date("Jul 8, 2005 UTC").toISOString()        // "2005-07-08T00:00:00.000Z"
new Date("2005-07-08").toISOString()             // "2005-07-08T00:00:00.000Z"

Eliminé la Date.parse()llamada ya que se usa automáticamente en un argumento de cadena. También comparé las fechas con el formato ISO8601 para que pueda comparar visualmente las fechas entre sus fechas locales y las fechas UTC. Los tiempos están separados por 7 horas, que es la diferencia de zona horaria y por qué sus pruebas mostraron dos fechas diferentes.

La otra forma de crear estas mismas fechas locales / UTC sería:

new Date(2005, 7-1, 8)           // "2005-07-08T07:00:00.000Z"
new Date(Date.UTC(2005, 7-1, 8)) // "2005-07-08T00:00:00.000Z"

Pero todavía recomiendo Moment.js, que es tan simple pero potente :

// parse string
moment("2005-07-08").format()       // "2005-07-08T00:00:00+02:00"
moment.utc("2005-07-08").format()   // "2005-07-08T00:00:00Z"
// year, month, day, etc.
moment([2005, 7-1, 8]).format()     // "2005-07-08T00:00:00+02:00"
moment.utc([2005, 7-1, 8]).format() // "2005-07-08T00:00:00Z"
DJDaveMark
fuente
0

La respuesta aceptada de CMS es correcta, acabo de agregar algunas características:

  • recortar y limpiar espacios de entrada
  • analizar barras, guiones, dos puntos y espacios
  • tiene día y hora predeterminados

// parse a date time that can contains spaces, dashes, slashes, colons
function parseDate(input) {
    // trimes and remove multiple spaces and split by expected characters
    var parts = input.trim().replace(/ +(?= )/g,'').split(/[\s-\/:]/)
    // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
    return new Date(parts[0], parts[1]-1, parts[2] || 1, parts[3] || 0, parts[4] || 0, parts[5] || 0); // Note: months are 0-based
}
Stephane L
fuente