web-dev-qa-db-fra.com

Pourquoi Date.parse donne-t-il des résultats incorrects?

Premier cas:

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

Sortie:

Vendredi 08 juil 2005 00:00:00 GMT-0700 (PST)

Deuxième cas:

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

Sortie:

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


Pourquoi la deuxième analyse est-elle incorrecte?

306
user121196

Jusqu'à la parution de la 5ème édition, la méthode Date.parse était complètement dépendante de implementation (new Date(string) est équivalent à Date.parse(string) sauf que cette dernière retourne un nombre plutôt qu'une Date). Dans la cinquième édition, la spécification a été ajoutée afin de prendre en charge un simplifié (et légèrement incorrect) ISO-8601 (voir aussi Quelles sont les chaînes de date et heure valides dans JavaScript? ). Mais à part cela, il n'y avait que no exigence sur ce que Date.parsenew Date(string) devrait accepter, à part le fait qu'ils devaient accepter n'importe quelle sortie _/Date # toString (sans préciser de quoi il s'agissait).

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

A partir d'ECMAScript 2019 (édition 9), le format de Date # toString et Date # toUTCString , a été spécifié comme suit:

  1. jj MMM JJ AAAA HH: mm: SS ZZ [(nom du fuseau horaire)]
    par exemple. Mardi 10 juillet 2018 18h39:58 GMT + 0530 (IST)
  2. jj, JJ MMM AAAA HH: mm: SS Z
    par exemple. Mar 10 juil 2018 13:09:58 GMT

fournir 2 formats supplémentaires que Date.parse doit analyser 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 year, month et day pour éviter toute ambiguïté:

// parse a date in yyyy-mm-dd format
function parseDate(input) {
  var 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
}
418
CMS

Au cours de ma récente expérience en tant qu’interprète JS, j’ai beaucoup débattu du fonctionnement interne des dates ECMA/JS. Donc, je pense que je vais mettre mes 2 cents ici. Espérons que partager ces informations aidera les autres à poser des questions sur les différences entre les navigateurs dans la manière 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 représentant le nombre de millisecondes écoulées depuis le 1/1/1970 UTC (GMT correspond à l'UTC). Les dates postérieures à 1/1/1970 00:00:00 sont des nombres positifs et les dates antérieures sont négatives.

Par conséquent, le code suivant produit exactement le même résultat sur tous les navigateurs.

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

Dans mon fuseau horaire (EST), le résultat est 18000000 car c'est le nombre de ms en 5 heures (seulement 4 heures par heure d'été). La valeur sera différente dans différents fuseaux horaires. Tous les principaux navigateurs le font de la même manière.

Voici le problème cependant. Bien que les principaux navigateurs analysent les dates dans les formats de chaîne d'entrée, les formats de chaîne d'entrée varient quelque peu, mais ils les interprètent essentiellement de la même manière en ce qui concerne les fuseaux horaires et l'heure d'été. Le seul à retenir est le format ISO 8601. C'est le seul format décrit dans la spécification ECMA-262 v.5 spécifiquement. Pour tous les autres formats de chaîne, l'interprétation dépend de la mise en œuvre. Ironiquement, c'est le format où les navigateurs peuvent différer. Voici un résultat de comparaison entre Chrome et Firefox du 1/1/1970 sur ma machine, au format de chaîne ISO 8601.

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
  • Le spécificateur "Z" indique que l'entrée est déjà en heure UTC et ne nécessite aucun décalage avant son stockage. 
  • Le spécificateur "-0500" indique que l'entrée est au format GMT-05: 00, de sorte que les deux navigateurs L'interprètent comme appartenant à mon fuseau horaire local. Cela signifie que la valeur Est convertie en UTC avant d’être stockée. Dans mon cas, cela signifie ajouter 18000000ms à la valeur interne de la date, ce qui nécessite un décalage de -18000000ms (-05: 00) pour me ramener à l'heure locale.
  • Cependant, en l'absence de spécificateur, FF considère l'entrée comme une heure locale, tandis que Chrome La traitait comme une heure UTC. Pour moi, cela crée une différence de 5 heures dans la valeur stockée, ce qui est problématique. Dans mon implémentation, je me suis retrouvé ici avec FF parce que j'aime bien que la sortie de toString corresponde à ma valeur d'entrée, sauf si je spécifie un fuseau horaire différent, ce que je ne fais jamais. le absence d'un spécificateur doit présumer que l'heure locale est entrée.

Mais c’est là que la situation s’aggrave: FF traite la forme abrégée du format ISO 8601 ("AAAA-MM-JJ") différemment de la forme longue ("AAAA-MM-JJTHH: mm: ss: sssZ") pour aucune raison logique que ce soit. Voici la sortie de FF avec les formats de date ISO long et court sans spécificateur de fuseau horaire.

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

Donc, pour répondre directement à la question du demandeur initial, "YYYY-MM-DD" est la forme abrégée du format ISO 8601 "YYYY-MM-DDTHH:mm:ss:sssZ". Ainsi, il est interprété comme heure UTC, tandis que l'autre est interprété comme local. C'est pourquoi, 

Cela ne jive pas:

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

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 du bas est la suivante pour l'analyse des chaînes de date. La SEULE chaîne ISO 8601 que vous pouvez analyser en toute sécurité sur tous les navigateurs est la forme longue. Et, utilisez TOUJOURS le spécificateur "Z". Si vous le faites, vous pouvez sans risque vous déplacer 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());

Heureusement, la plupart des navigateurs actuels traitent les autres formats d'entrée de la même manière, y compris les formats les plus fréquemment utilisés, à savoir "1/1/1970" et "1/1/1970 00:00:00 AM". Tous les formats suivants (et autres) sont traités comme une entrée d’heure locale dans tous les navigateurs et convertis au format UTC avant stockage. Ainsi, les rendant compatibles entre navigateurs. La sortie de ce code est la même dans tous les navigateurs de mon fuseau horaire.

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

Le côté sortie

Du côté de la sortie, tous les navigateurs convertissent les fuseaux horaires de la même manière, mais ils traitent les formats de chaîne différemment. Voici les fonctions toString et ce qu’elles produisent. Notez que les fonctions toUTCString et toISOString sont émises à 5h00 du matin sur ma machine.

Convertit l'heure UTC en heure locale avant l'impression

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

Imprime directement l'heure UTC enregistrée

 - toUTCString
 - toISOString 

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

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

Je n'utilise normalement pas le format ISO pour la saisie de chaînes. L’utilisation de ce format ne m’apporte des avantages que lorsque les dates doivent être triées sous forme de chaînes. Le format ISO est triable tel quel, alors 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.

181
drankin2112

Il y a une méthode à la folie. En règle générale, si un navigateur peut interpréter une date comme une 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 donc il est analysé dans l'heure locale.

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

70
danvk

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

Cette méthode est utile pour la date formatée 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'];
7
guido rizzi

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: "Si vous le pouvez, tenez-vous-en à" AAAA/MM/JJ "pour vos chaînes de date. Il est universellement pris en charge et sans ambiguïté. Avec ce format, toutes les heures sont en local." J'ai défini des tests: http://jsfiddle.net/jlanus/ND2Qg/432/ Ce format: + évite l'ambiguïté des ordres du jour et du mois en utilisant y m d ordre et une année à 4 chiffres + évite que le problème UTC par rapport au problème local ne soit pas conforme au format ISO en utilisant des barres obliques + danvk, le mec dygraphs , dit que ce format convient à tous les navigateurs. 

5
Juan Lanus

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 troisième argument détermine l'analyse stricte (disponible à partir de la version 2.3.0). Sans cela, moment.js peut également donner des résultats incorrects.

5
Lukasz Wiktor

Alors que CMS est correct que le passage de chaînes dans la méthode d'analyse est généralement dangereux, la nouvelle spécification ECMA-262 5e édition (également appelée ES5) de la section 15.9.4.2 suggère que Date.parse() doit en réalité gérer les dates au format ISO . L'ancienne spécification ne contenait aucune revendication de ce type. 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 l'indique Date.prototype.toISOString(), mais elle est représentée dans votre fuseau horaire local.

4
peller

Cette bibliothèque d'analyse de date poids léger devrait résoudre tous les problèmes similaires. J'aime la bibliothèque car elle est assez facile à agrandir. 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 à 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
Nux

Les deux sont corrects, mais ils sont interprétés comme des dates avec deux fuseaux horaires différents. Alors vous avez 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 supprimé l'appel Date.parse() puisqu'il est utilisé automatiquement sur un argument de chaîne. J'ai également comparé les dates en utilisant format ISO8601 afin que vous puissiez comparer visuellement les dates entre vos dates locales et les dates UTC. Les heures sont séparées de 7 heures, ce qui correspond à la différence de fuseau horaire et explique pourquoi vos tests ont montré deux dates différentes.

L’autre moyen 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 fortement Moment.js qui est comme simple mais 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"
2
DJDaveMark

Voici un extrait court et flexible permettant de convertir une chaîne datetime de manière sécurisée pour tous les navigateurs, comme indiqué 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 que Date.parse avec:

(new Date(tstring)).getTime()
2
Lorenz Lo Sauer

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, les tirets, les deux points et les espaces
  • a jour et 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
}
0
Stephane L