Pourquoi Date.parse donne des résultats incorrects?


345

Premier cas:

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

Production:

Ven.08 juil.2005 00:00:00 GMT-0700 (PST)

Deuxième cas:

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

Production:

Jeu 07 juil 2005 17:00:00 GMT-0700 (PST)


Pourquoi la deuxième analyse est-elle incorrecte?


30
La deuxième analyse n'est pas incorrecte en soi, c'est juste que la première est analysée en heure locale et la seconde en UTC. Notez que "Thu Jul 07 2005 17:00:00 GMT-0700 (PST)" est identique à "2005-07-08 00:00".
jches


18

1
Au cas où quelqu'un viendrait ici pour comprendre pourquoi une date revient NaNdans Firefox, j'ai découvert que la plupart des autres navigateurs (et Node.js) analyseront une date sans jour, comme "avril 2014" comme 1er avril 2014, mais Firefox renvoie NaN. Vous devez passer une date appropriée.
Jazzy

Pour ajouter au commentaire de Jason ci-dessus: Si vous recevez un NaN dans Firefox, un autre problème pourrait être que Firefox et Safari n'aiment pas les dates avec trait d'union. Seul Chrome le fait. Utilisez plutôt une barre oblique.
Bangkokian

Réponses:


451

Jusqu'à la sortie de la 5e édition, la Date.parseméthode était complètement dépendante de l'implémentation ( new Date(string)équivaut à ce Date.parse(string)que ce dernier renvoie un nombre plutôt qu'un a Date). Dans la 5e édition, l'exigence a été ajoutée pour prendre en charge une norme ISO-8601 simplifiée (et légèrement incorrecte) (voir également Que sont les chaînes de date-heure valides en JavaScript? ). Mais à part cela, il n'y avait aucune exigence pour ce que Date.parse/ new Date(string)devrait accepter à part le fait qu'ils devaient accepter n'importe quelle Date#toStringsortie (sans dire ce que c'était).

Depuis ECMAScript 2017 (édition 8), les implémentations devaient analyser leur sortie pour Date#toStringet Date#toUTCString, mais le format de ces chaînes n'était pas spécifié.

Depuis ECMAScript 2019 (édition 9), le format de Date#toStringet Date#toUTCString, a été spécifié comme (respectivement):

  1. jjj MMM JJ AAAA HH: mm: ss ZZ [(nom du fuseau horaire)]
    par exemple mar. 10 juil. 2018 18:39:58 GMT + 0530 (IST)
  2. ddd, DD MMM YYYY HH: mm: ss Z
    par ex. Mar.10 juil.2018 13:09:58 GMT

fournissant 2 autres formats qui Date.parse devraient être analysés de manière fiable dans les nouvelles implémentations (notant que la prise en charge n'est pas omniprésente et que les implémentations non conformes resteront utilisées pendant un certain temps).

Je recommanderais que les chaînes de date soient analysées manuellement et que le constructeur Date soit utilisé avec les arguments année, mois et jour pour éviter toute ambiguïté:

// 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
}

Excellent, j'ai dû utiliser cela car Date.parseil ne se comportait pas avec les formats de date britanniques pour une raison quelconque, je n'ai pas pu travailler
Ben

1
Les parties temporelles sont documentées dans le code @CMS. J'ai utilisé ce code avec un format de date "2012-01-31 12:00:00" return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);Fonctionne parfaitement, merci!
Richard Rout

1
@CMS qu'entendez-vous par dépendant de l'implémentation ?
Royi Namir

3
@RoyiNamir, cela signifie que les résultats dépendent du navigateur Web (ou d'une autre implémentation JavaScript) qui exécute votre code.
Samuel Edwin Ward

1
J'ai également eu un problème avec la nouvelle date (chaîne) dans différents navigateurs se comportant différemment. Il n'est même pas question qu'il soit cassé sur les anciennes versions d'IE, les différents navigateurs ne sont tout simplement pas cohérents. N'utilisez jamais Date.parse ou une nouvelle Date (chaîne).
Hoffmann

195

Au cours de ma récente expérience dans l'écriture d'un interprète JS, j'ai beaucoup lutté avec le fonctionnement interne des dates ECMA / JS. Donc, je pense que je vais jeter mes 2 cents ici. Espérons que le partage de ces informations aidera les autres à répondre à toutes les questions sur les différences entre les navigateurs dans la façon dont ils traitent les dates.

Le côté entrée

Toutes les implémentations stockent leurs valeurs de date en interne sous forme de nombres 64 bits qui représentent le nombre de millisecondes (ms) depuis le 01/01/1970 UTC (GMT est la même chose que UTC). Cette date est l'époque ECMAScript qui est également utilisée par d'autres langages tels que Java et les systèmes POSIX comme UNIX. Les dates survenant après l'époque sont des nombres positifs et les dates antérieures sont négatives.

Le code suivant est interprété comme la même date dans tous les navigateurs actuels, mais avec le décalage de fuseau horaire local:

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

Dans mon fuseau horaire (EST, qui est -05: 00), le résultat est 18000000 car c'est le nombre de ms en 5 heures (c'est seulement 4 heures pendant les mois d'heure d'été). La valeur sera différente selon les fuseaux horaires. Ce comportement est spécifié dans ECMA-262 donc tous les navigateurs le font de la même manière.

Bien qu'il existe une certaine variation dans les formats de chaîne d'entrée que les principaux navigateurs analyseront comme des dates, ils les interprètent essentiellement de la même manière en ce qui concerne les fuseaux horaires et l'heure d'été, même si l'analyse est largement dépendante de l'implémentation.

Cependant, le format ISO 8601 est différent. C'est l'un des deux seuls formats décrits dans ECMAScript 2015 (ed 6) spécifiquement qui doivent être analysés de la même manière par toutes les implémentations (l'autre est le format spécifié pour Date.prototype.toString ).

Mais, même pour les chaînes au format ISO 8601, certaines implémentations se trompent. Voici une sortie de comparaison de Chrome et Firefox lorsque cette réponse a été initialement écrite pour 1/1/1970 (l'époque) sur ma machine en utilisant des chaînes de format ISO 8601 qui devraient être analysées exactement à la même valeur dans toutes les implémentations:

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
  • Dans le premier cas, le spécificateur "Z" indique que l'entrée est en temps UTC donc n'est pas décalée de l'époque et le résultat est 0
  • Dans le deuxième cas, le spécificateur "-0500" indique que l'entrée est en GMT-05: 00 et les deux navigateurs interprètent l'entrée comme étant dans le fuseau horaire -05: 00. Cela signifie que la valeur UTC est décalée de l'époque, ce qui signifie ajouter 18000000 ms à la valeur de l'heure interne de la date.
  • Le troisième cas, où il n'y a pas de spécificateur, doit être traité comme local pour le système hôte. FF traite correctement l'entrée comme heure locale tandis que Chrome la traite comme UTC, produisant ainsi des valeurs d'heure différentes. Pour moi, cela crée une différence de 5 heures dans la valeur stockée, ce qui est problématique. D'autres systèmes avec différents décalages obtiendront des résultats différents.

Cette différence a été corrigée à partir de 2020, mais d'autres bizarreries existent entre les navigateurs lors de l'analyse des chaînes de format ISO 8601.

Mais ça empire. Une particularité d'ECMA-262 est que le format de date uniquement ISO 8601 (AAAA-MM-JJ) doit être analysé en UTC, tandis que ISO 8601 exige qu'il soit analysé en tant que local. Voici la sortie de FF avec les formats de date ISO longs et courts sans spécificateur de fuseau horaire.

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

Ainsi, le premier est analysé comme local car il s'agit de la date et de l'heure ISO 8601 sans fuseau horaire, et le second est analysé en UTC car il s'agit uniquement de la date ISO 8601.

Ainsi, pour répondre directement à la question d'origine, "YYYY-MM-DD"ECMA-262 doit être interprété comme UTC, tandis que l'autre est interprété comme local. Voilà pourquoi:

Cela ne produit pas de résultats équivalents:

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

Cela fait:

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

La ligne de fond est la suivante pour analyser les chaînes de date. La SEULE chaîne ISO 8601 que vous pouvez analyser en toute sécurité entre les navigateurs est la forme longue avec un décalage (± HH: mm ou "Z"). Si vous le faites, vous pouvez aller et venir en toute sécurité entre l'heure locale et l'heure UTC.

Cela fonctionne sur tous les navigateurs (après IE9):

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

La plupart des navigateurs actuels traitent les autres formats d'entrée de la même manière, y compris le '1/1/1970' (M / D / YYYY) fréquemment utilisé et le '' 1/1/1970 00:00:00 AM '' (M / D / YYYY hh : mm: ss ap) formats. Tous les formats suivants (sauf le dernier) sont traités comme des entrées d'heure locale dans tous les navigateurs. La sortie de ce code est la même dans tous les navigateurs de mon fuseau horaire. Le dernier est traité comme -05: 00 quel que soit le fuseau horaire de l'hôte car le décalage est défini dans l'horodatage:

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"));

Cependant, étant donné que l'analyse même des formats spécifiés dans ECMA-262 n'est pas cohérente, il est recommandé de ne jamais compter sur l'analyseur intégré et de toujours analyser manuellement les chaînes, par exemple en utilisant une bibliothèque et en fournissant le format à l'analyseur.

Par exemple, dans moment.js, vous pourriez écrire:

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

Côté sortie

Côté sortie, tous les navigateurs traduisent les fuseaux horaires de la même manière mais ils gèrent les formats de chaîne différemment. Voici les toStringfonctions et ce qu'elles produisent. Notez le toUTCStringet la toISOStringsortie des fonctions 5h00 sur ma machine. En outre, le nom du fuseau horaire peut être une abréviation et peut être différent dans différentes implémentations.

Conversion de l'heure UTC en heure locale avant l'impression

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

Imprime l'heure UTC stockée directement

 - toUTCString
 - toISOString 

Dans Chrome
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

Dans 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

Normalement, je n'utilise pas le format ISO pour l'entrée de chaîne. La seule fois où l'utilisation de ce format est bénéfique pour moi, c'est lorsque les dates doivent être triées sous forme de chaînes. Le format ISO est triable tel quel tandis que les autres ne le sont pas. Si vous devez avoir une compatibilité entre navigateurs, spécifiez le fuseau horaire ou utilisez un format de chaîne compatible.

Le code new Date('12/4/2013').toString()passe par la pseudo-transformation interne suivante:

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

J'espère que cette réponse a été utile.


3
Tout d'abord, ceci est une rédaction fantastique. Je voulais cependant souligner une dépendance. En ce qui concerne les spécificateurs de fuseau horaire, vous avez déclaré: "L'absence d'un spécificateur devrait présumer une entrée d'heure locale." Heureusement, la norme ECMA-262 supprime tout besoin de présumer. Il déclare : "La valeur d'un décalage de fuseau horaire absent est" Z "." Ainsi, une chaîne de date / heure sans fuseau horaire spécifié est supposée être un UTC plutôt qu'une heure locale. Bien sûr, comme avec tant de choses JavaScript, il semble y avoir peu d'accord entre les implémentations.
Daniel

2
... y compris les formats '1/1/1970' et '1/1/1970 00:00:00 AM' les plus fréquemment utilisés. - le plus fréquemment utilisé ? Ce n'est pas dans mon pays, c'est sûr.
ulidtko

2
@ulidtko - Désolé, je suis aux États-Unis. Wow ... vous êtes juste là à Kiev. J'espère que vous et votre famille restez en sécurité et que les choses se stabiliseront bientôt là-bas. Prenez soin de vous et bonne chance avec tout.
drankin2112

Juste une note ici. Il semble que cela ne fonctionne pas pour les navigateurs Safari (c'est-à-dire iOS ou OSX). Cela ou j'ai un autre problème en cours.
keyneom

1
@ Daniel: heureusement, les auteurs d'ECMAScript ont corrigé leur erreur concernant le fuseau horaire manquant pour les représentations de date et d'heure. Désormais, les chaînes de date et d'heure sans fuseau horaire utilisent le décalage du fuseau horaire de l'hôte (c'est-à-dire "local"). De façon confuse, les formulaires de date ISO 8601 uniquement sont traités comme UTC (même si ce n'est pas particulièrement clair d'après les spécifications), tandis que ISO 8601 les traite comme locaux, donc ils n'ont pas tout résolu.
RobG

70

Il y a une méthode à la folie. En règle générale, si un navigateur peut interpréter une date comme ISO-8601, il le fera. "2005-07-08" tombe dans ce camp, et donc il est analysé comme UTC. "8 juillet 2005" ne peut pas, et il est donc analysé dans l'heure locale.

Voir JavaScript et les dates, quel gâchis! pour plus.


3
" En règle générale, si un navigateur peut interpréter une date comme ISO-8601, il le sera. " N'est pas supportable. "2020-03-20 13:30:30" est traité comme ISO 8601 et local par de nombreux navigateurs, mais Invalid Date par Safari. Il existe de nombreux formats ISO 8601 qui ne sont pas pris en charge par la plupart des navigateurs, par exemple 2004-W53-7 et 2020-092.
RobG

7

Une autre solution consiste à créer un tableau associatif au format de date, puis à reformater les données.

Cette méthode est utile pour les dates formatées de manière inhabituelle.

Un exemple:

    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'];

5

Utilisez moment.js pour analyser les dates:

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

Le 3ème argument détermine une analyse stricte (disponible à partir de 2.3.0). Sans cela, moment.js peut également donner des résultats incorrects.


4

Selon http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html le format "aaaa / mm / jj" résout les problèmes habituels. Il dit: "Respectez" AAAA / MM / JJ "pour vos chaînes de date chaque fois que possible. Il est universellement pris en charge et sans ambiguïté. Avec ce format, toutes les heures sont locales." J'ai mis des tests: http://jsfiddle.net/jlanus/ND2Qg/432/ Ce format: + évite l'ambiguïté de l'ordre du jour et du mois en utilisant la commande ymd et une année à 4 chiffres + évite le problème UTC vs local non se conformer au format ISO en utilisant slashes + danvk, le gars des dygraphes , dit que ce format est bon dans tous les navigateurs.


Vous voudrez peut-être voir la réponse de l'auteur .
Brad Koch

Je dirais que la solution avec l'exemple de jsFiddle fonctionne assez bien si vous utilisez jQuery car il utilise l'analyseur de datepicker. Dans mon cas, le problème est avec jqGrid, mais j'ai trouvé qu'il avait sa méthode parseDate. Mais de toute façon l'exemple m'a aidé en me donnant une idée donc +1 pour ça, merci.
Vasil Popov

2
Cet article sur les dygraphes est faux et le premier exemple de la page illustre clairement pourquoi l'utilisation de barres obliques au lieu de tirets est vraiment un mauvais conseil. Au moment où l'article a été écrit, l'utilisation du «13/03/2012» a conduit le navigateur à l'analyser comme une date locale, au lieu de l'UTC. La spécification ECMAScript définit la prise en charge explicite de l'utilisation de "YYYY-MM-DD" (ISO8601), utilisez donc toujours des tirets. Il convient de noter qu'au moment où j'écris ce commentaire, Chrome a été corrigé pour traiter les barres obliques comme UTC.
Lachlan Hunt

4

Bien que CMS ait raison de dire que la transmission de chaînes dans la méthode d'analyse n'est généralement pas sûre, la nouvelle spécification ECMA-262 5e édition (aka ES5) de la section 15.9.4.2 suggère qu'en Date.parse()réalité, elle devrait gérer les dates au format ISO. L'ancienne spécification ne faisait pas une telle réclamation. Bien sûr, les anciens navigateurs et certains navigateurs actuels ne fournissent toujours pas cette fonctionnalité ES5.

Votre deuxième exemple n'est pas faux. Il s'agit de la date spécifiée en UTC, comme le laisse entendre Date.prototype.toISOString(), mais elle est représentée dans votre fuseau horaire local.


1
Et l'analyse des chaînes de date a été à nouveau modifiée dans ECMAScript 2015 afin que "2005-07-08" soit local, pas UTC. BTW, ES5 n'était la norme qu'en juin 2011 (et actuellement ECMAScript 2015 l'est). ;-)
RobG

1
Juste pour confondre les choses, TC39 a décidé en octobre (juste un mois après mon post précédent) que "2005-07-08" devrait être UTC , cependant "" 2005-07-08T00: 00: 00 "devrait être local. Les deux sont ISO 8601 formats compatibles, les deux sont sans fuseau horaire, mais sont traités différemment. Allez comprendre
RobG

2

Cette bibliothèque d'analyse de date légère devrait résoudre tous les problèmes similaires. J'aime la bibliothèque car elle est assez facile à étendre. Il est également possible de l'i18n (pas très simple, mais pas si difficile).

Exemple d'analyse:

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

Et le formatage revient à la chaîne (vous remarquerez que les deux cas donnent exactement le même résultat):

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") );

2

Voici un extrait court et flexible pour convertir une chaîne datetime d'une manière sûre pour tous les navigateurs, comme nicel l'a détaillé par @ 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);

Votre navigateur doit fournir le même résultat d'horodatage Date.parsequ'avec:

(new Date(tstring)).getTime()

Je suggérerais d'ajouter T à l'expression régulière pour attraper également les dates déjà formatées JS: inputTimestamp.split (/ [T \ /: -] / g)
andig

Si vous divisez la chaîne en composants, la prochaine étape la plus fiable consiste à utiliser ces composants comme arguments du constructeur Date. La création d'une autre chaîne à donner à l'analyseur vous ramène simplement à l'étape 1. "2014-04-29 13:00:15" doit être analysé en tant que local, votre code le reformate en UTC. :-(
RobG

2

Les deux sont corrects, mais ils sont interprétés comme des dates avec deux fuseaux horaires différents. Vous avez donc comparé des pommes et des oranges:

// 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"

J'ai enlevé le Date.parse() appel car il est utilisé automatiquement sur un argument de chaîne. J'ai également comparé les dates en utilisant le format ISO8601 afin que vous puissiez comparer visuellement les dates entre vos dates locales et les dates UTC. Les heures sont espacées de 7 heures, ce qui est la différence de fuseau horaire et pourquoi vos tests ont montré deux dates différentes.

L'autre façon de créer ces mêmes dates locales / UTC serait:

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"

Mais je recommande toujours vivement Moment.js qui est aussi simple que puissant :

// 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"

0

La réponse acceptée de CMS est correcte, je viens d'ajouter quelques fonctionnalités:

  • rogner et nettoyer les espaces d'entrée
  • analyser les barres obliques, tirets, deux-points et espaces
  • a le jour et l'heure par défaut

// 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
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.