Chaînes de date : frontières entre heure locale et UTC
Une sonde sur les chaînes de date : une date seule se lit comme minuit UTC alors qu’une date-heure sans décalage se lit comme heure locale, et un jour hors limites déborde.
BotBrowser Team
Vous voulez la documentation structurée pour Empreinte ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
BotBrowser permet de rejouer un fixture de même origine autorisé sous un profil déclaré et de garder une trace bornée de l’horodatage exact qu’une page a produit, mais il ne peut pas faire que deux chaînes nomment le même instant, ne peut pas empêcher un jour hors limites de déborder, ni faire trier une chaîne stockée comme trie son instant. Cette revue exécute une sonde sur dix cas fixes, avec America/Toronto, et consigne quelles formes Date.parse lit comme UTC et lesquelles comme heure locale, comment quatre formes qui épellent le même jour donnent trois instants, ce que font un jour hors limites et un trou d’heure d’été, pourquoi un tri par chaîne et un tri par instant divergent, et à quelle heure locale chaque forme tombe.
Résumé
- Une chaîne de date seule se lit comme minuit UTC, donc la sonde imprime
"2026-10-31" -> 2026-10-31T00:00:00.000Zetutc-equal -> Date.UTC=1793404800000 parse=1793404800000. - Une date-heure sans décalage se lit comme heure locale, donc
"2026-10-31T00:00:00"devient2026-10-31T04:00:00.000Zsur un hôte à UTC-4. - Quatre formes qui épellent le même jour donnent
distinct -> 3 instants from 4 shapes, parce que la forme date seule est UTC et que les formes à décalage déplacent l’instant. - Un jour hors limites déborde : la sonde demande le 30 février et obtient le 2 mars, et
"2026-02-30"s’analyse en 2 mars, pas en erreur. - Une heure murale dans un trou d’heure d’été avance, donc
02:30devient03:30, tandis qu’un repli garde l’une des deux occurrences de01:30. - Un tri par chaîne et un tri par instant divergent : la sonde imprime
sort-by-ms -> 2026-10-31 , 2026-10-30T23:00:00 , ...contresort-by-string -> 2026-10-30T23:00:00 , 2026-10-31 , .... Une seconde comparaison que la sonde expose : deux chaînes peuvent nommer le même instant et se trier différemment, le même écart que tri de tableau et comparateurs traite pour un comparateur qui n’est pas un ordre total cohérent. La formeZse lit pareil dans toute zone, la forme avec heure glisse avec le décalage de l’hôte, et la forme date seule est stable mais tombe à une autre heure locale.
Sommaire
- Quelles formes se lisent comme UTC et lesquelles comme locales
- Pourquoi quatre écritures d’un jour font trois instants
- Ce qui déborde et ce qui échoue
- Pourquoi un tri par chaîne et un tri par instant divergent
- Consigner une lecture d’analyse de date
Quelles formes se lisent comme UTC et lesquelles comme locales
Les mêmes champs de calendrier peuvent nommer plusieurs instants, et la forme choisit lequel, pas la date. La sonde analyse quatre chaînes et imprime l’instant ISO et le temps numérique produits :
"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 première ligne fixe la règle d’une date nue. "2026-10-31" s’analyse en 2026-10-31T00:00:00.000Z, et la dernière ligne le confirme : utc-equal montre que la valeur analysée égale exactement Date.UTC(2026, 9, 31). Une chaîne de date seule n’a donc aucune lecture locale ; c’est minuit UTC par définition, et une page qui la reformate avec un formateur local sur un hôte à UTC-4 affichera 30 octobre, 20:00, la veille pour un lecteur qui attendait le 31.
La deuxième ligne fixe la règle d’une date-heure sans décalage. "2026-10-31T00:00:00" s’analyse en 04:00Z sur cet hôte, parce que le décalage absent est rempli avec la zone de l’hôte, UTC-4 à cette date. La valeur n’est donc pas le même instant que la chaîne de date seule, et une page qui mélange les deux formes dans un jeu de données a deux points de départ pour ce qui ressemble à un seul jour.
La troisième ligne est la forme sans ambiguïté. "2026-10-31T00:00:00Z" s’analyse en 00:00Z, sur tout hôte, parce que le Z final nomme UTC explicitement. Un décalage numérique explicite fait de même : la forme -04:00 des lignes shape: imprime 04:00Z, et la forme +09:00 imprime la veille à 15:00Z, toutes deux le même instant quel que soit l’hôte.
La quatrième ligne est risquée. "2026-10-31 00:00:00" utilise un espace au lieu d’un T, et la sonde montre qu’elle s’analyse en 04:00Z, la même lecture locale que la forme avec T. La forme à espace marche donc pareil sur cet hôte, mais elle est définie par l’implémentation et non requise, donc un moteur peut la lire autrement ou la rejeter. Les seules formes qu’une application devrait stocker sont la date seule, qui est UTC, et une date-heure avec Z ou un décalage explicite. Comment un instant stocké devient une date visible est une étape à part, et formatage date-heure et portabilité des fuseaux consigne ce qu’un formateur fait de la zone qu’il reçoit.
Pourquoi quatre écritures d’un jour font trois instants
La ligne distinct -> 3 instants from 4 shapes résume le problème. Quatre chaînes qu’un lecteur appellerait « 31 octobre » donnent trois instants, parce que deux des quatre nomment le même instant par deux chemins.
La forme date seule est 00:00Z. La forme sans décalage est 04:00Z sur cet hôte, quatre heures plus tard. La forme -04:00 est aussi 04:00Z, donc elle coïncide avec la forme sans décalage par coincidence de la zone de l’hôte, pas par règle. La forme +09:00 est 15:00Z la veille, cinq heures plus tôt que la paire 04:00Z. Les quatre chaînes couvrent donc de la veille au jour demandé, et une page qui les traite comme interchangeables en ordonnera deux mal et en placera deux autres la veille.
La coïncidence entre la forme sans décalage et la forme -04:00 est le piège. Sur un hôte à UTC-4 elles impriment le même instant, donc un test qui ne tourne que là passe, et le même code sur un hôte à UTC+9 imprime deux instants différents pour les deux formes. Un test qui veut prouver que le code ne dépend pas de la zone doit tourner dans au moins deux zones, ou affirmer contre des formes Z et à décalage qui ne dépendent pas de l’hôte.
L’instant qu’une chaîne stockée nomme est donc une propriété de la forme et, pour les formes non marquées, de la zone du lecteur. C’est pourquoi une trace doit stocker l’instant comme nombre ou chaîne Z plutôt qu’une heure murale, et pourquoi un affichage qui montre une date doit formater depuis l’instant avec une zone nommée plutôt que relire l’heure murale que la chaîne impliquait.
La ligne offset-now -> 240 fait le même point depuis l’autre côté. La date 2026-10-31 est à l’heure d’été sur cet hôte, donc son décalage est -4. Un hôte qui la formaterait avec un décalage fixe -05:00 d’hiver serait une heure faux pour cette date et juste en hiver, le défaut qu’un décalage fixe écrit dans un fixture cache jusqu’à ce que la date traverse une transition.
Les deux formes restantes sont celles qu’un filtre de tableau de bord produit. "2026" s’analyse en 2026-01-01T00:00:00.000Z et "2026-10" en 2026-10-01T00:00:00.000Z, en UTC parce qu’une date ISO partielle n’a ni heure ni décalage. Un filtre qui envoie un mois en 2026-10 nomme donc le premier instant de ce mois en UTC, qui est le mois précédent pour un lecteur à l’ouest de Greenwich : ici la lecture locale est 2026-09-30T20:00, un libellé de mois qui montre le mauvais mois. Un tableau de bord qui veut « octobre » pour un utilisateur de Toronto doit envoyer un début explicite comme 2026-10-01T00:00:00-04:00 plutôt qu’un mois nu, ou envoyer le mois en texte et le formater sans le convertir en instant.
Les formes "10/31/2026" et "Oct 31 2026" se comportent à l’inverse et montrent pourquoi une chaîne formatée par la langue ne doit jamais être le format de stockage. Les deux s’analysent en 2026-10-31T04:00:00.000Z ici, une lecture locale, donc elles suivent la zone de l’hôte comme la forme sans décalage. Mais leur acceptation est définie par l’implémentation, donc un moteur qui suit un autre ordre de date peut lire 10/31/2026 comme le dixième du trente-et-unième mois, ou la rejeter. Une page qui stocke une date au format que sa langue imprime et l’analyse ensuite sur un serveur dans une autre langue a une forme acceptée ici et lue autrement là, et le défaut apparaît comme un mauvais mois plutôt qu’une erreur.
La ligne d’aller-retour répond si une heure murale survit au stockage. La sonde analyse 2026-10-31T09:30:00, imprime l’instant 2026-10-31T13:30:00.000Z, relit localHours=9, et confirme que le nombre stocké rend le même instant. C’est un aller-retour correct sur un hôte, et c’est exactement la forme qui casse sur un autre : la même chaîne lue dans une zone à UTC+1 tombe à 08:30Z, donc l’instant stocké bouge de cinq heures et l’heure murale affichée reste 09:30 seulement parce que l’affichage utilise la même zone que l’analyse. Une page qui stocke l’heure murale et la reformate dans la même zone ne voit pas d’erreur, et l’erreur n’apparaît que quand les deux extrémités tournent dans des zones différentes.
Ce qui déborde et ce qui échoue
Toute date invalide n’échoue pas, et la sonde sépare les formes qui débordent de celles qui lèvent :
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
Le constructeur Date fait déborder un jour hors limites sur le mois suivant. new Date(2026, 1, 30) demande le 30 février, qui n’existe pas en année commune, et le résultat est le 2 mars, parce que le constructeur ajoute les jours en trop au dernier jour valide de février. Idem pour le 29 février en année non bissextile : new Date(2026, 1, 29) devient le 1 mars. Une page qui construit une date depuis trois nombres analysés obtient donc un débordement silencieux, et le getMonth() du résultat n’est pas le mois passé.
L’analyse de la forme chaîne fait de même. new Date('2026-02-30') rend 2026-03-02T00:00:00.000Z au lieu d’Invalid, parce que la chaîne se lit comme une date seule et que le jour déborde. C’est le contraire de ce que beaucoup de validateurs supposent, et cela signifie qu’un contrôle écrit if (isNaN(Date.parse(value))) laisse passer une chaîne qu’aucun calendrier ne contient. Une validation qui doit rejeter le 30 février doit comparer les champs analysés aux champs d’entrée plutôt que faire confiance à l’analyseur.
Les cas d’heure d’été montrent le même ajustement silencieux. La ligne du trou demande 02:30 le jour du passage à l’heure d’été américain, une heure qui n’existe pas, et la sonde imprime 2026-03-08T07:30:00.000Z avec l’heure locale 03:30. Le moteur a avancé l’heure murale inexistante du trou, donc la date construite est une heure plus tard que demandé et le getHours() du résultat est 3, pas 2. La ligne du repli demande 01:30 le jour du retour à l’heure d’hiver, une heure qui se produit deux fois, et la sonde imprime 2026-11-01T05:30:00.000Z avec la lecture locale 01:30, une des deux occurrences et pas forcément celle qu’un lecteur voulait.
Une page qui programme une tâche à une heure murale locale hérite donc des deux bords. Au printemps elle peut démarrer une heure plus tard parce que l’heure demandée n’existait pas, et à l’automne elle peut démarrer à la première de deux occurrences, ce qui va pour une tâche qui tourne une fois et pas pour une qui compte les occurrences. La trace d’une telle tâche doit stocker l’instant choisi, pas l’heure murale demandée, parce que l’heure murale n’identifie pas l’exécution de façon unique.
Pourquoi un tri par chaîne et un tri par instant divergent
Les deux tris de la même liste sont la conséquence la plus claire, parce qu’ils divergent sur l’ordre des mêmes quatre chaînes :
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
Trier par instant met 2026-10-31 en tête, parce que c’est minuit UTC de ce jour, et 2026-10-30T23:00:00 ensuite, parce que c’est l’heure murale locale de la veille lue comme 03:00Z du lendemain. Trier par chaîne met 2026-10-30T23:00:00 en tête, parce que les caractères 2026-10-30 trient avant 2026-10-31. Les deux tris divergent donc sur les deux premiers éléments, et la divergence vient entièrement des formes, pas des valeurs.
Un comparateur qui n’est pas un ordre total cohérent donne un ordre faux mais silencieux, la même défaillance que documente tri de tableau et comparateurs. Le tri par défaut d’un tableau est le tri par chaîne, parce qu’il compare les unités de code. Une liste de dates triée avec le sort() par défaut est donc triée par son texte, ce qui coïncide avec l’ordre chronologique seulement quand chaque élément a la même forme. Mélangez une valeur date seule et une valeur date-heure et l’ordre texte et l’ordre chronologique divergent, sans avertissement.
La zone qu’une chaîne sérialisée nomme est aussi celle qu’un guide des fuseaux et de la langue doit résoudre avant de pouvoir étiqueter une date. Le correctif est de trier par le nombre analysé et de stocker une seule forme. La ligne sort-by-ms est ce correctif, et elle ordonne les mêmes quatre chaînes par l’instant qu’elles nomment. Une page qui stocke chaque horodatage comme chaîne Z ou nombre et trie sur cette valeur obtient le même ordre d’un tri par chaîne et d’un tri numérique, parce que la forme est uniforme ; une page qui mélange les formes obtient deux ordres et ne peut pas dire lequel un lecteur verra.
Les lignes local-hour montrent le même problème côté affichage. local-hour:2026-10-31 -> 20 lit le minuit UTC de la valeur date seule dans la zone de l’hôte et rapporte 20:00 de la veille, tandis que local-hour:2026-10-31T00:00:00 -> 0 lit la forme locale et rapporte 00:00, et local-hour:2026-10-31T00:00:00Z -> 20 lit la forme Z et rapporte le même 20:00 que la valeur date seule. Deux des trois formes s’affichent donc le soir précédent, et seule la forme locale s’affiche comme le minuit demandé. Une trace qui garde l’heure affichée sans la forme ne peut pas expliquer pourquoi deux pages qui disent « 31 octobre » affichent des heures différentes.
Les lignes compare et diff-hours bouclent. La sonde imprime compare -> '2026-10-31' == new Date('2026-10-31') is true, ce qui dit seulement que la chaîne analysée s’égale, puis diff-hours -> dateOnlyUtcHour=0 dateTimeUtcHour=4, qui montre que les deux formes tombent à quatre heures d’écart en UTC sur cet hôte. Une comparaison entre une valeur date seule et une valeur date-heure n’est donc pas une comparaison du même jour ; c’est un instant à 00:00Z contre un instant à 04:00Z sur cet hôte, qui diffèrent bien que les deux chaînes commencent par la même date.
Consigner une lecture d’analyse de date
Une lecture d’analyse de date soutient une affirmation bornée : ce moteur, cette zone, ces formes ont produit ces instants. Quatre champs la gardent vérifiable :
- La forme, parce qu’une valeur date seule est UTC et une date-heure sans décalage est locale, et la forme ne se voit pas dans l’instant.
- La zone de l’hôte, parce que la forme sans décalage en dépend alors que les formes
Zet à décalage non, et une lecture sans la zone ne se reproduit pas. - L’instant en UTC, parce que l’heure murale qu’un lecteur voit est une seconde étape qui peut différer par zone.
- La clé de tri et la forme stockée, parce qu’un tri par chaîne et un tri par instant d’une liste mixte divergent, et une trace doit dire lequel a produit l’ordre.
Sources publiques
- MDN : Date
- MDN : Date.parse()
- MDN : Date.UTC()
- ECMAScript : format de chaîne date-heure
- MDN : Intl.DateTimeFormat
- IANA : base de données tz
- BotBrowser : référence de cohérence d’automatisation
Conclusion pratique
Stockez une seule forme et stockez-la comme instant : une chaîne Z ou un nombre, jamais une heure murale locale, parce qu’une valeur date seule et une valeur date-heure sans décalage nomment des instants différents et que la seconde dépend de la zone du lecteur. Ne comptez pas sur Date.parse pour rejeter un jour impossible, puisque le 30 février déborde vers le 2 mars au lieu de rendre Invalid ; comparez les champs analysés à l’entrée. Triez sur le nombre analysé plutôt que sur la comparaison de chaînes par défaut quand une liste peut porter plusieurs formes, et rappelez-vous qu’une heure murale dans un trou d’heure d’été avance tandis qu’une dans un repli est une de deux occurrences. Pour la moitié affichage du même problème, lisez formatage date-heure et portabilité des fuseaux ; pour les données de langue qui nomment une zone, lisez fuseaux horaires et langue.
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
Formatage des dates et portabilité des fuseaux horaires
EmpreinteICU : alias de fuseau et la TZ que vous rapportez
EmpreinteTri de tableau et comparateurs : un piège subtil
EmpreinteUnicode : quatre identités de texte lues par une page
EmpreinteAnalyse d’URL et des search params
EmpreinteIntl.Collator : l’ordre de locale face aux unités de code
Faites passer BotBrowser de la recherche à la production
Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.