Dates : confusion entre UTC, GMT, Locale, etc.

Le tout entre Backend Mysql et frontend javascript

a marqué ce sujet comme résolu.

Bonjour,

J’ai passé un temps fou à entrer des horaires en base de données et je crois que je vais devoir tout recommencer. Please Help.

A la base :

  • un formulaire permet de créer des horaires d’ouverture et de fermeture d’un lieu
  • la ligne est entrée en base de données mysql
  • dans le frontend, je récupère ces horaires pour un lieu, et je les affiche.

Le problème :

  • le formulaire utilise VUE (ie. javascript).
  • l’application utilise API Platform (et Symfony pour coordonner le tout)
  • quand je crée l’objet à envoyer à l’api pour l’entrer en BDD, j’utilise :

new Date('les données entrées via l'UI').toISOString()

Quand je console.log l’objet, j’ai donc des données comme ceci :

{ day: "2021-12-17", openingTime: "2021-12-17T08:00:00.000Z", closingTime: "2021-12-17T17:00:00.000Z" }

Alors que l’utilisateur a choisi 9h00 / 18h00. Je retrouve en base de données :

2021-12-27 2021-12-27 08:00:00 2021-12-27 17:00:00

La structure de la table est dans l’ordre date / datetime / datetime. Quand je double-clique sur le closingTime dans Mysql pour éditer les données, il m’indique en bas Time Zone +0200.

Dans le frontend, lors du réaffichage des données, API Platform me renvoie :

"openingTime": "2021-12-17 08:00:00", "closingTime": "2021-12-17 17:00:00",

L’information de la timezone semble perdue en route. Du coup, quand je réaffiche la donnée formatée pour l’utilisateur, l’heure devient incorrecte.

Dans config.yml, j’ai :

Symfony\Component\Serializer\Normalizer\DateTimeNormalizer: arguments: $defaultContext: datetime_format: 'Y-m-d H:i:s' datetime_timezone: 'Europe/Paris'

Comment est-ce que je peux rattraper le coup ? Dois-je tout recommencer ? Comment configurer API Platform pour qu’il me redonne les bonnes heures ?

Merci pour votre aide.

Hello,

Par défaut, API Platform retourne les informations au format ISO-8601, que JavaScript gère nativement (d’où le .toISOString()). Je t’encourage à conserver ce format qui permet de gérer correctement les fuseaux horaires sans avoir à faire quoi que ce soit, dans toute ton appli Symfony.

Pour cela, il te faut retirer la configuration datetime_format du fichier de configuration.

+0 -0

Salut

Je note que dans config.yaml tu dis avoir ceci :

Symfony\Component\Serializer\Normalizer\DateTimeNormalizer:
  arguments:
    $defaultContext:
      datetime_format: 'Y-m-d H:i:s'
      datetime_timezone: 'Europe/Paris'

As-tu essayé d’ajouter O (o majuscule — ou alors P), voire tout remplacer par c, pour datetime_format ?

Attention au fait que toISOString() de JavaScript convertit en UTC (qui est la "nouvelle" dénomination pour GMT), donc en JavaScript, au moment de rédiger ce message, on aurait ceci :

const date = new Date();
console.log(date, date.toISOString());
// Tue Sep 07 2021 18:09:27 GMT+0200 (heure d’été d’Europe centrale)
// 2021-09-07T16:09:27.429Z

Je pense qu’il pourrait suffire de spécifier comme quoi le fuseau horaire voyage d’un côté et de l’autre pour pouvoir sinon faire disparaître, au moins réduire les soucis. sachant que JavaScript sait très bien créer des objets avec Date('2020-08-06 16:02:451+0200').

+0 -0

Merci, déjà, de me rassurer : j’avais bien fait de faire ceci pour enregistrer les données. Bon, maintenant, pour la récupération. Si je fais comme deuchnord dit, à savoir commenter la configuration dans Symfony, je reçois ce genre de données par API Platform :

openingTime "2021-12-17T08:05:00+01:00" closingTime "2021-12-17T17:05:00+01:00"

console.log(new Date(item.openingTime).toLocaleString()); //17/12/2021, 08:05:00

Par contre, je remarque que si la chaine devient "2021–12–17T08:05:00.000Z", Vue me réaffiche la bonne heure avec .toLocaleTimeString(). Il en faudrait relativement peu pour que je modifie "à la main" les données lors de l’affichage pour remplacer le "+01:00" par "Z"…Je vais me donner encore la matinée pour comprendre comment faire ça bien.

Mfff. C’est quand même bien prise de tête, ces histoires, non ? Ou c’est juste moi ?

Edit : je vois que "p" (en PHP) est censé me donner ce Z dans la sortie.

Edit2: "RangeError: invalid date" avec 'Y-m-d H:i:sp' ; il faut dire que je reçois "openingTime "2021–12–17 08:05:00p", ce qui est très très utile ^^ ; php > 8, ce n’est pas le cas de mon serveur.

Edit3 : bon, je confirme :

console.log(new Date("2021-12-17T08:05:00+01:00").toLocaleString());
//8:05 -> Incorrect
 console.log(new Date("2021-12-17T08:05:00Z").toLocaleString());
//9:05 -> Correct

Que fais-je de travers ? C’est au niveau de JS que ça foire ?

+0 -0

Par contre, je remarque que si la chaine devient "2021–12–17T08:05:00.000Z", Vue me réaffiche la bonne heure avec .toLocaleTimeString(). Il en faudrait relativement peu pour que je modifie "à la main" les données lors de l’affichage pour remplacer le "+01:00" par "Z"…Je vais me donner encore la matinée pour comprendre comment faire ça bien.

En fait, tu n’as même pas besoin de faire cela, la plupart des langages modernes de haut niveau (dont PHP) gèrent parfaitement ce genre de problématique nativement sans action nécessaire de ta part (ce qui est une bonne chose, car comme tu le dis, la gestion des fuseaux horaire est une tannée) ! :D

Edit : je vois que "p" (en PHP) est censé me donner ce Z dans la sortie.

Pas tout à fait, le p correspond ici au fuseau horaire dans lequel la date a été fournie, en remplaçant UTC par Z (pour Zoulou, qui est le terme militaire utilisé pour désigner le fuseau UTC, et qui est valide en ISO-8601).

+0 -0

Navré mais du coup je ne comprends pas : comment transformer les données reçues "2021–12–17T08:05:00+01:00" en 9h05 ? JE n’arrive sincèrement pas à comprendre si c’est au niveau du formatage d’api platform via le services.yaml que ça se joue ou si c’est au niveau de la sortie en JS avec le trillion de fonctions que ça doit se corriger ?

C’est au niveau de l’affichage, et donc de JavaScript, que ça se passe, mais là on arrive à la limite de mes compétences, je laisse donc quelqu’un d’autre répondre sur ce sujet ^^

Ton API est là pour fournir les informations dont a besoin ton front, et seulement ça, et c’est à ce dernier (qui peut être une page Web, une application mobile, etc) de les manipuler et les adapter.

Ça sera d’autant plus bénéfique que ton API n’aura pas à se préoccuper du fuseau horaire dans lequel elle est supposée retourner tes dates, ton front ayant déjà l’information nativement.

+0 -0

Pas forcément. Ton back reçoit également de la part du client ses informations régionales : fuseau horaire et format d’affichage de date. Par conséquent, il est capable d’afficher les heures (au format texte brut par contre) selon le client.

Après, je ne sais pas si c’est ce qu’il y a de mieux à faire.

+0 -0

comment transformer les données reçues 2021–12–17T08:05:00+01:00 en 9h05

Ton problème est surtout que tu n’as pas la bonne date en base de données. Si ta base de donnée contient une date qui est 8h05 dans le fuseau de Paris en hiver (ce qu’indique le +01:00), tu auras beau le tordre comme tu veux, ce sera toujours 8h05 dans le fuseau de Paris en hiver.

Ton problème n’est pas à la lecture de la BDD, ni à l’affichage des données, mais à son enregistrement. Tu devrais avoir 2021–12–17T08:05:00Z ou bien 2021–12–17T09:05:00+01:00 dans ta base de données pour avoir une date à 9h05 heure de Paris.

+2 -0

J’avais préparé un pavé, mon VDD l’a résumé  :D

Note que si ta base de données ne permet pas de spécifier le fuseau horaire directement avec la date, alors il faut soit l’enregistrer à part (nouvelle colonne), soit t’assurer que tout ce que tu enregistres est dans un fuseau horaire défini (il semble que ce soit UTC si j’en crois les remarques précédentes). Mais dans ce dernier cas, il ne faut pas spécifier un autre fuseau horaire quand on récupère la date depuis la base de données. Or, avec PHP comme avec JavaScript, si ce n’est pas précisé, ce n’est pas forcément le fuseau horaire attendu qui est utilisé — d’autant plus avec les changements d’heure. Il ne faut pas prendre le fuseau horaire local courant par défaut.

+0 -0

Donc c’est sur Symfony que j’ai loupé quelque chose ? Ou Api Platform ? Il y avait un endroit où je devais dire spécifiquement de respecter les données entrées ? Je n’avais jamais eu ce souci avant (c’est mon premier projet avec API Platform), du coup il ne m’était pas venu à l’esprit qu’un problème pourrait survenir de là.

En BDD, ce sont des champs "datetime" (* @ORM\Column(type="datetime", nullable=true)). Comment savoir si ma BDD permet de "spécifier le fuseau horaire" ou non ?

Edit : argh, je ne comprends vraiment pas ! Je fais des tests, je crée des horaires : ils sont sous la forme ""2021–09–09T07:00:00.000Z"" lors de l’envoi (9h00 entrés via l’UI) ; dans ma BDD ils apparaissent comme "2021–09–10 07:00:00" ; quand je double-clique sur la case pour éditer l’horaire, j’ai bien dans le panneau qui apparait "Time Zone +0200". C’est donc bien que l’information est enregistrée quelque part, non ?

+0 -0

MySQL n’enregistre rien du fuseau horaire, pour le moins avec les versions 5, je n’ai pas pratiqué la version 8. Mais si tu n’es pas avec MySQL, c’est peut-être différent.

Par contre, Doctrine met en garde sur les histoires de fuseaux horaires. Le souci étant là que la date enregistrée est probablement la bonne au niveau de l’intention, mais quand il y a récupération, c’est le fuseau horaire par défaut qui est pris, mais évidemment sans faire la correction parce que Doctrine n’a aucune idée de comment la faire ni même s’il faut la faire.

+0 -0

Je réalise seulement maintenant :

Quand mon utilisateur - se trouvant en France - entre des horaires via l’UI de 8h du matin à midi pour le 24 septembre, le script utilise la fonction "toISOString()" et renvoie "2021–09–24T06:00:00+02:00" / 2021–09–24T10:00:00+02:00" -> cette information est incorrecte, c’est bien ça ? Ca devrait être "2021–09–24T08:00:00+02:00 / 2021–09–24T12:00:00+02:00" ?

C’est à ce moment que se crée le décalage qui fait tout foirer ?

Bon. Il vaut mieux se rendre compte maintenant du souci que lors de la mise en production, hein. Mais vous n’imaginez pas à quel point je suis énervé. Je reprends donc calmement. respire à fond

Via les api/docs (donc sans mon interface JS/Vue qui pourrait être la cause de mon souci) :

1- les horaires sont automatiques quand on clique sur try it out ; je POST ceci à 11h57 (depuis la France) :

{
  "day": "2021-09-09T09:57:49.273Z",
  "openingTime": "2021-09-09T09:57:49.273Z",
  "closingTime": "2021-09-09T09:57:49.273Z",
  "activity": "/api/activities/36"
}

2- j’ai comme Response Body :

{
  "@context": "/api/contexts/OpeningHours",
  "@id": "/api/opening_hours/2928",
  "@type": "OpeningHours",
  "id": 2928,
  "day": "2021-09-09T00:00:00+02:00",
  "openingTime": "2021-09-09T09:57:49+02:00",
  "closingTime": "2021-09-09T09:57:49+02:00",
  "activity": "/api/activities/36"
}

Première étape : est-ce que tout est normal à ce point ?

3- je vais voir sur phpmyadmin le résultat dans la table ; j’ai :

2928 36 2021-09-09 2021-09-09 09:57:49 2021-09-09 09:57:49

Ici : quand je clique pour éditer les deux derniers champs, j’ai "Time zone+0200" ; je précise que la configuration de mysql est celle par défaut, je n’ai jamais mis les doigts là-dedans.

Seconde étape : là encore, est-ce normal ?

4- Maintenant, je GET cette même ligne toujours via Api/Docs ; j’obtiens :

{
  "@context": "/api/contexts/OpeningHours",
  "@id": "/api/opening_hours/2928",
  "@type": "OpeningHours",
  "id": 2928,
  "day": "2021-09-09T00:00:00+02:00",
  "openingTime": "2021-09-09T09:57:49+02:00",
  "closingTime": "2021-09-09T09:57:49+02:00",
  "activity": "/api/activities/36"
}

Là, on dirait bien que l’info est correctement conservée, non ? Il y a juste le "Z" initial transformé en "+02:00".

5- Maintenant, je veux donc afficher pour un humain normal de nouveau 11h57 depuis "2021–09–09T09:57:49+02:00" ; je fais donc :

console.log(new Date("2021-09-09T09:57:49+02:00"))
//Date Thu Sep 09 2021 09:57:49 GMT+0200 (heure d’été d’Europe centrale)

console.log(new Date("2021-09-09T09:57:49+02:00").toLocaleString())
//09/09/2021, 09:57:49

Et là, je n’ai pas le bon résultat. Comme si le "+02:00" était ignoré. Et si je le remplace par Z, je retrouve le bon 11h57.

Je ne comprends pas ce que je fais de travers.

Merci.

Première question pour pouvoir t’aider, parce que j’ai l’impression en te lisant que le problème vient de là : quel type de « date » tu as besoin de gérer ? (d’un point de vue purement fonctionnel, en ignorant tout ce qui existe côté technique).

Première étape : non, le retour possède déjà le mauvais fuseau horaire, ce qui est envoyé n’est pas la même chose que ce qui est retourné. Attention, c’est justement le point du tutoriel qui t’a été proposé : 2021-09-09T09:57:49.273Z n’est pas la même chose que 2021-09-09T09:57:49.273+02:00, parce que cette dernière équivaut à 2021-09-09T07:57:49.273Z.

Deuxième étape : là, difficile de savoir. Il faudrait enregistrer des dates à l’heure d’été et des dates à l’heure d’hiver pour savoir si le décalage en base est toujours le même ou s’il s’adapte.

Troisième étape : non, du fait du souci de la première étape.

Quatrième étape (point 5) , je m’étais trompé : pour reconstruire une date en JavaScript, il faut faire new Date(Date.parse( la chaîne au format ISO )). Le constructeur de l’objet Date en JavaScript ne prend qu’un entier, et cela explique en partie le souci sur cet exemple de code. Quand bien même, le +02:00 apparaît comme ignoré parce que c’est le fuseau horaire local par défaut de ton navigateur.

+1 -0

Je vous jure que j’essaye réellement de comprendre, hein (ce n’est peut-être pas évident pour vous). Merci réellement de m’aider sur ce coup.

Pour répondre à SpaceFox, le type de date qui m’intéresse, selon le tuto, serait la date locale dépendante de l’utilisateur : je stocke des horaires d’activités qui ont lieu en France dans des plages horaires précises et qui exigent que l’utilisateur soit dans la même zone géographique pour en profiter (référentiel implicite ?). Savoir que ça correspond à 4h du matin en Indonésie n’apporte rien.

Edit : grâce à la remarque d’Ymox précédente qui souligne que dès l’introduction en BDD ça foire, je regarde le log via le Symfony Profiler et dans Doctrine je découvre pour un test réalisé à 14:33 :

INSERT INTO opening_hours (day, opening_time, closing_time, activity_id) VALUES (?, ?, ?, ?)

Parameters:

[▼
  1 => "2021-09-09"
  2 => "2021-09-09 12:33:52"
  3 => "2021-09-09 12:33:52"
  4 => 36
]

Est-ce que ça aide d’une manière ou d’une autre à comprendre d’où vient mon problème ? Doctrine ? Symfony ? PHP sur mon PC ? API Platform ?

Edit 2 : En entrant juste sans les T / Z :

{
  "day": "2021-09-09",
  "openingTime": "2021-09-09 14:42:35.354",
  "closingTime": "2021-09-09 14:42:35.354",
  "activity": "/api/activities/36"
}

j’ai l’enregistrement correct en BDD.

Devant ma totale incompréhension du problème et de son origine, ne vaut-il pas mieux que je laisse complètement tomber cette histoire de toISOString() et de UTC, et que je formate les données entrées par l’utilisateur sans aucune info de ce type ?

Question subsidiaire : a priori, tous mes enregistrements actuels en BDD sont faux, donc. Y a-t-il moyen de les corriger sans devoir tout re-rentrer ?

+0 -0

OK, alors la première chose à faire, c’est de vérifier que tu as une version de MySQL supérieure à la 8.0.19, sans quoi tu ne pourras pas gérer correctement les fuseaux horaires :

Beginning with MySQL 8.0.19, you can specify a time zone offset when inserting TIMESTAMP and DATETIME values into a table. The offset is appended to the time part of a datetime literal, with no intravening spaces, and uses the same format used for setting the time_zone system variable, with the following exceptions: […]

La doc de MySQL 8

Partant de là, tu peux déjà vérifier que quand tu rentres « le 9 septembre 2021 à 21h03 UTC+2 » dans la base, il enregistre bien exactement ça et pas une conversion bizarre ou une version tronquée. Ça implique d’avoir la bonne version de MySQL et le bon type de colonne de stockage.

Ensuite, il faut fouiller dans la doc de tes outils (Doctrine et Vue au moins) pour vérifier que les types et méthodes de conversion que tu utilises comprennent bien cette histoire de fuseau horaire sans faire de conversion ni ignorer le fuseau, parce que parfois on a des surprises – surtout avec les bindings de BDD, qui sont historiquement assez lents à suivre les nouveautés d’un côté ou de l’autre.

Si toute la chaine est compatible, tu es bon.

Si tu as un trou dans la raquette (typiquement une conversion sauvage ou les informations de fuseau horaire ignorées), tu as plusieurs possibilités, et là il n’y a que la connaissance du fonctionnel qui permettra de répondre :

  • La technique ancienne avec MySQL qui consiste à ajouter une colonne par date pour stocker le fuseau horaire (mais les comparaisons de dates en SQL ne fonctionneront plus correctement si le fuseau est différent).
  • Considérer que tu peux en fait gérer des instants (les instants d’ouverture et de fermeture d’après tes exemples), qui sont gérés intégralement avec des timestamp avec conversion à l’affichage le plus tard possible (convertis en date selon le fuseau horaire de ton utilisateur, ou de l’entité qui ouvre/ferme selon ce que tu veux afficher).
  • Considérer que tu peux en fait gérer des dates dépendantes de l’utilisateur (les dates et heures d’ouverture et de fermeture, dans le fuseau horaire local de l’entité concernée), avec l’information du fuseau horaire stockée avec l’entité (et pas avec les dates) et affichée séparément.

Le choix entre tout ça va énormément dépendre des calculs et comparaisons que tu vas vouloir faire sur tes dates.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte