Evénements récurrents et fuseaux horaires

La mise en place d'événements récurrents dans un calendrier semble de prime abord être une tâche simple, en fait, lorsque la récurrence doit pouvoir être appliquée dans différents fuseaux horaires, c'est beaucoup plus complexe...

La difficulté est double pour automatiser des telles récurrences : il s’agit de définir les informations minimales à conserver en mémoire afin de pouvoir appliquer cette récurrence, et aussi de prendre en compte les problèmes classiques liés à la représentation du temps. Problèmes loin d’être triviaux, pour s’en convaincre se référer l’article précédent concernant la représentation du temps en javascript (local et UTC), et celui sur les fuseaux horaires.

Commençons par définir les trois types principaux de récurrence que nous avons identifiés :
-    Récurrence en temps UTC (l’événement se passe toujours à la même heure en temps UTC)
-    Récurrence en temps local (l’événement a lieu toujours à la même heure en temps local pour un fuseau horaire donné)
-    Récurrence en temps relatif (l’événement a lieu toujours à la même heure, dans le fuseau ho-raire où se trouve l’utilisateur)

Dans le premier cas, Récurrence en temps UTC, pour un fuseau horaire donné, l’heure effective de l’événement change en cours d’année lors du passage de l’heure d’hiver à l’heure d’été et inversement. C’est ce cas que nous avions implanté dans un premier temps, mais manque de chance, çà n’est pas le plus utile dans la vie de tous les jours.

Le deuxième cas, Récurrence en temps local, est le plus courant, prenons l’exemple du rendez-vous hebdomadaire d’un cadre Parisien avec sa direction, rendez-vous fixé de 10h à midi tous les lundis sur une période allant du 1er janvier au 31 décembre.

Le dernier cas,  Récurrence en temps relatif, correspond à « à 7h tous les matins, je fais mon jogging ». Même si je me déplace à San Francisco, je ferais quand même mon jogging à 7h.

- Cas 1 : Récurrence en temps UTC


  • Informations minimales permettant de définir intuitivement une règle de récurrence :

|  dtStart  |  dtEnd  |  startTime  |  endTime  |  autres  |

•    dtStart : la date UTC de début d’application de la récurrence
•    dtEnd : la date UTC de fin d’application de la récurrence
•    startTime : l’heure de début de l'événement en temps UTC au format hh:mm
•    endTime : l’heure de fin de l'événement en temps UTC au format hh:mm
•    autres : les autres informations nécessaires (type d’événement, journées d’application, etc…)

Ces données permettent de définir que des récurrences appliquées en UTC, autrement dit notre rendez-vous se produisant de 10h à 12h tous jours de la semaine à Paris (GMT+1 en janvier) sera visualisé entre un 6h et 8h pour un collaborateur brésilien (GMT-3). Cependant lors du passage à l’heure d’été,  le rendez-vous glissera de 11h à 13h  (GMT+2 en avril) pour notre collaborateur Parisien et les horai-res varieront différemment au Brésil en fonction de la date d’application du changement d’heure été/hivers fixée par décret d’une année à l’autre.

  • Limites de cette représentation :

Les décalages horaires liés aux heures d’été et d’hivers ne sont pas gérables en UTC pour maintenir la cohérence des horaires de début (startTime) et de fin (endTime) d’application d’une récurrence. Dans notre exemple la récurrence aura donc lieu entre 10h à 12h toute l’année à Paris.

D’un autre point de vue la date est fixée en UTC, son comportement par rapport au référentiel définit en UTC est donc logique et peut être nécessaire dans certains cas (exemple : relevés sismiques à effec-tuer à intervalles réguliers pour ne pas fausser des statistiques, …).

- Cas 2 : Récurrence en temps local

Dans ce cas, il est nécessaire de conserver l’information concernant le fuseau horaire concerné. En effet, le décalage horaire entre le temps local et le temps UTC varie plusieurs fois en cours d’année (horaires saisonniers) et de façon différente d’un fuseau horaire à l’autre (cf. article sur le calcule du décalage horaire d'une date à partir du fuseau horaire).

  • Ajout d’une information « timeZone » pour palier au problème des décalages horaires :

La tz_database (se référer à l’article précédent) définit pour chaque zone horaire les décalages par rapport à l’UTC, dans le passé, comme dans le futur.

Enregistrer la « timeZone » dans laquelle à été définie une récurrence et stocker les horaires de début (startTime) et de fin (endTime) en heure locale et non plus en UTC permet de pallier à ces problème.

  • Informations minimales permettant de définir une règle de récurrence :

|  dtStart  |  dtEnd  |  startTime  |  endTime  |  timeZone  |  autres  |

•    dtStart : la date UTC de début d’application de la récurrence
•    dtEnd : la date UTC de fin d’application de la récurrence
•    startTime : l’heure de début d’un élément en temps local de la timeZone au format hh:mm
•    endTime : l’heure de fin d’un élément en temps local de la timeZone au format hh:mm
•    timeZone : la zone horaire locale au format de la tz_database (exemple : Europe/Paris)
•    autres : les autres informations nécessaires (type d’élément, journées d’application, etc.)

Reprenons l’exemple précédent, le rendez-vous de notre cadre est désormais visualisable dans toutes les zones prises en compte à l’heure correspondant  à la date réelle.

  • Voyons comment effectuer en javascript le calcul de conversion à la date du lundi 1er mars 2010 à partir des informations suivantes :

•    startTime  = 10 :00
•    endTime = 12 :00
•    timeZone = « Europe/Paris »

Un appel à getTimeZoneDate (dateStart, "Europe/Paris") avec la dateStart égale à « Mon Mar 01 2010 10:00:00 GMT+2 » rendra la date de départ réelle du rendez-vous pour le lundi 1er mars 2010 en fonction de l’heure du navigateur (qui n’est autre que l’heure système sous Windows+Firefox).
/**
* Rend la date locale pour une date et une timeZone de reference
* @param date : la date de reference a verifier
* @param timeZone : la timezone a verifier
* @return date : la date correcte pour le timeZone de reference
*/

getTimeZoneDate = function (date, timeZone) {
var tzo = getRealTimeZoneOffset(date, timeZone);
var result = new Date(date.getTime() + (tzo * 60 * 1000));
return result;
}

/**
* Rend l'offset a appliquer a une date pour une timeZone de reference
* @param date : la date de reference a verifier
* @param timeZone : la timezone a verifier
* @return offset : la difference en minutes entre la timezone et la date courante
*/

getRealTimeZoneOffset = function (date, timeZone) {
var tzo = getTimeZoneOffset(date, timeZone);
return - (tzo * 60) - new Date(date.getTime()).getTimezoneOffset();
}

La « timeZoneOffset » est capable de nous donner le décalage horaire par rapport a l’UTC de n’importe quelle date quelle que soit l'année. Plutôt que l'implantation réelle qui passe par l'utilisation effective de la tz_database, nous présentons ici, à titre explicatif, une implémentation limitée. Cette implémentation se contente de considérer que le changement d’horaire saisonnier pour la timeZone « Europe/Paris » aura lieu le dernier dimanche de mars pour l’heure d’été et le dernier dimanche d’octobre pour l’heure d’hiver.
/**
* Rend l'offset a appliquer a une date pour une timeZone de reference
* @param date : la date de reference a verifier
* @param timeZone : la timezone a verifier
* @return offset : la timezone offset souhaitee en minutes
*/

getTimeZoneOffset = function (date, timeZone) {
var tzo;
var startDate;
var endDate;
var currentYear = date.getFullYear();
switch (timeZone) {
case "Europe/Paris" :
// passage à l’heure d’été
startDate = getLastDayOfMonth(0, 2, currentYear);
startDate.setHours(1);
// passage à l’heure d’hiver
endDate = getLastDayOfMonth(0, 9, currentYear);
endDate.setHours(1);
// @date est en été ou en hiver ?
if (date.getTime() >= startDate.getTime() && date.getTime() < endDate.getTime())
tzo = 2;
else
tzo = 1;
break;
default :
tzo = 0;
break;
}
return tzo;
}

/**
* Rend le dernier jour souhaite d'un mois pour une annee
* (exemple: pour obtenir le dernier dimanche du mois de mars 2010, appeler avec les paramètres suivants "getLastDayOfMonth(0, 2, 2010);"
* @param dayOffset : l'offset du dernier jour souhaite (entre 0 et 6)
* @param month : le mois dont on cherche le dernier jour (entre 0 et 11)
* @param year : l'annee de la recherche
* @return date : la date du dernier jour souhaite d'un mois pour une annee
*/

getLastDayOfMonth = function (dayOffset, month, year) {
var oneDay = 1000*60*60*24;
var lastDay = new Date(year, (month+1)%12, 1, 12, 0, 0);
lastDay = new Date(lastDay.getTime() - oneDay);
var offset = lastDay.getDay();
if (offset>dayOffset) lastDay = new Date(lastDay.getTime() - (offset * oneDay));
return lastDay;
}

- Cas 3 : Récurrence en temps relatif

Nous n’aborderons pas ce cas qui nécessite uniquement de se caler sur l’heure locale du navigateur.

Calculer le décalage horaire d’une date à partir du fuseau horaire

Problématique

Nous avons vu précédemment que nativement, il n’est pas possible en javascript de créer des dates autrement que dans le fuseau horaire du navigateur (qui est donné par l’OS sous-jacent) ou en temps UTC.

Si l’on veut créer une date (complète, c'est-à-dire précisant aussi l’heure) dans un autre fuseau horaire, il est possible de le faire en précisant l’offset de ce fuseau horaire par rapport au temps UTC.

Ainsi, si la variable utc contient une date UTC.

On commence par convertir la date UTC en millisecondes (via l’api getTime() qui donne le nombre de millisecondes depuis le 1er janvier 1970).

// la date UTC (en ms)
var utc = maDateUTC.getTime() ;

avec la variable timeZoneOffset qui contient l’offset de la zone horaire exprimé en minutes.

On convertit le timeZoneOffset en millisecondes (il y a 60000 ms dans 1 minute) :

// l’offset en millisecondes
var msOffset=timeZoneOffset * 60000 ;

myDate, qui contient la conversion en UTC, est construite à partir du nombre de millisecondes total

var msLocal=utc+msOffset ;
var myDate=new Date(msLocal) ;

Rappelons que l’api getTimeZoneOffset, rend le nombre de minutes (positif ou négatif) de décalage du temps local du navigateur par rapport à UTC.

En parsant une date au format ISO 8601 (par exemple 2010-07-16T19:20:30,4+01:00) et en convertissant le décalage horaire associé et en s’inspirant du code précédent, il est donc possible de convertir en UTC une date dont on connaît le décalage horaire.

Déterminer le décalage horaire d’une date en fonction de la zone horaire

Être capable de convertir en UTC une date dont on connaît le décalage horaire, c’est bien. Être capable de déterminer ce décalage horaire à partir d’un fuseau horaire, c’est mieux ! Ce décalage horaire n’est généralement pas fixe puisqu’il dépend du décalage heure d’été/heure d’hiver. Il est donc nécessaire de déterminer, pour la zone horaire considérée, si la date qu’on souhaite créer est en horaire d’été ou d’hiver et le décalage horaire associé.

La tz database

Nous avions fait allusion aux tables de la tz database qui sont très utilisées. Cette base indique les modifications des zones horaires (y compris les changements d’horaire saisonnier) aussi bien passées que futures (dans les limites où il est possible de l’anticiper ; par exemple, au Brésil, le changement d’horaire saisonnier est fixé par décret).

La tz database est maintenue par des volontaires. Les commentaires des fichiers sources de la base donnent une indication de la difficulté pour rassembler ces informations, surtout pour les périodes historiques (ne serait ce que pour avoir les décalages horaires en France durant la dernière guerre suite au découpage en zone occupée/zone libre).

Dans la tz database, une zone horaire (ou time zone pour les anglo-saxons), est une zone géographique qui depuis 1970 est synchronisée en terme d’heure locale (y compris vis-à-vis des changements saisonniers). Cette zone géographique peut comporter plusieurs pays ou une région sous-nationale pour les certains pays étendus.

La tz database définit des conventions de nommage du type région/lieu (où région représente un continent, un océan ou la valeur spéciale Etc) pour l’identification unique des zones horaires (zone name). Cela donne par exemple America/New_York et Europe/Paris. Ces conventions utilisent les noms anglo-saxons (et pas locaux) et interdisent le caractère espace (au profit du tiret et du underscore). Dans certains cas, le lieu lui-même peut être composite (ce qui donne par exemple America/Indiana/Indianapolis).

La liste des idenfiants de zone horaire est disponible dans le fichier zone.tab à ftp://elsie.nci.nih.gov/pub/ . Par exemple la dernière version est contenue dans le fichier tzdata2010j.tar.gz. Ce fichier comporte pour chaque ligne (l’espace servant de séparateur) :

  • le code 2 caractères du pays (ISO 3166),
  • la latitude et longitude du lieu (en ISO 6709),
  • l’identifiant unique (zone name) et éventuellement des commentaires.

Voilà par exemple un extrait de zone.tab (les deux premières lignes comportent un commentaire) :

FM +0725+15147 Pacific/Truk Truk (Chuuk) and Yap
FM +0658+15813 Pacific/Ponape Ponape (Pohnpei)
FM +0519+16259 Pacific/Kosrae Kosrae
FO +6201-00646 Atlantic/Faroe
FR +4852+00220 Europe/Paris
GA +0023+00927 Africa/Libreville
GB +513030-0000731 Europe/London
GD +1203-06145 America/Grenada
GE +4143+04449 Asia/Tbilisi

Grâce à ces identifiants, un utilisateur peut donc choisir la zone horaire qu’il lui convient (et n’a donc plus à manipuler directement le décalage horaire par rapport à UTC).

Noter que Microsoft utilise d’autres conventions dans windows et qu’il existe une table de conversion des identifiants de zone horaire de windows à la tz database.. Pour ceux qui sont intéressés par les arcanes de la gestion des fuseaux horaires sous windows, voir l’article.

Utiliser les fichiers de la tz database

Il est bien sûr possible de parser les fichiers sources de la tz_database mais ils existent sous plusieurs forme compilées à http://code.google.com/p/tzdata/.

Le ficher tzdata_latest_all.zip disponible à cette adresse comporte en effet les formes compilées csv (excel), js, json, php, ruby et xml.

Par exemple, dans le sous-répertoire js/Europe figure le fichier paris.js. dont voilà un extrait :

{
"from": 1288486800000, // 2010-10-31T01:00:00Z
"to": 1301187600000, // 2011-03-27T01:00:00Z
"dst": false,
"offset": 3600,
"name": "CET"
}

Les propriétés de cette structure sont les suivantes :

  • from décrit (en millisecondes) la date à partir de laquelle l’offset s’applique (dans le fragment précédent, çà donne le 31 octobre à 1h en UTC).
  • to décrit (en millisecondes) la date jusqu’à laquelle l’offset s’applique (dans le fragment précédent, çà donne le 27 mars à 1h en UTC).
  • dst est un booléen à vrai si on est en heure d’été.
  • offset est le décalage horaire (en secondes) par rapport à l’UTC (dans le fragment précédent, on a dans cette période un décalage horaire de 3600 secondes, c'est-à-dire 1 heure)
  • name est le nom courant décrivant ce décalage horaire.

Dans le tableau suivant figure la liste des names utilisés en eurore (ce tableau est disponible dans le fichier source de la tz database décrivant l’europe) :

  • La première colonne donne le décalage horaire de l’heure d’hiver par rapport à l’UTC.
  • la colonne std décrit l’heure d’hiver,
  • la colonne dst l’heure saisonnière (daylight saving time) d’été (c'est-à-dire 1 heure de décalage en plus par rapport à l’heure d’hiver),
  • la colonne 2dst une seconde heure saisonnière (par exemple BDST est le British Double Summer Time utilisé en Angleterre de 1941 à 1945 et en 1947).


#              std dst  2dst
#              LMT           Local Mean Time
#  -4:00       AST ADT       Atlantic
#  -3:00       WGT WGST      Western Greenland*
#  -1:00       EGT EGST      Eastern Greenland*
#   0:00       GMT BST  BDST Greenwich, British Summer<
#   0:00       GMT IST       Greenwich, Irish Summer
#   0:00       WET WEST WEMT Western Europe
#   0:19:32.13 AMT NST       Amsterdam, Netherlands Summer (1835-1937)*
#   0:20       NET NEST      Netherlands (1937-1940)*
#   1:00       CET CEST CEMT Central Europe
#   1:00:14    SET           Swedish (1879-1899)*
#   2:00       EET EEST      Eastern Europe
#   3:00       MSK MSD       Moscow

En ce qui concerne la France, le name de l’heure d’hiver est CET (Central European Time) ; celui de l’heure d’été est CEST (Central European Summer Time).

Si on revient au fichier javascript décrivant paris, le fichier stocke un tableau global dans une variable locale zone (et comme le montre l'extrait suivant, il comporte théoriquement le timezone offset théoriquement jusqu’en 9999).


var zone = [
{
"from": -377711769600000, // -9999-01-01T00:00:00Z
"to": -2486678901000, // 1891-03-14T23:51:39Z
"dst": false,
"offset": 561,
"name": "LMT"
},


{
"from": 1269738000000, // 2010-03-28T01:00:00Z
"to": 1288486800000, // 2010-10-31T01:00:00Z
"dst": true,
"offset": 7200,
"name": "CEST"
},
{
"from": 1288486800000, // 2010-10-31T01:00:00Z
"to": 1301187600000, // 2011-03-27T01:00:00Z
"dst": false,
"offset": 3600,
"name": "CET"
},

{
"from": 2550704400000, // 2050-10-30T01:00:00Z
"to": 253402300799000, // 9999-12-31T23:59:59Z
"dst": false,
"offset": 3600,
"name": "CET"
}
] ;

En utilisant ces informations, il est donc possible de déterminer quel sera le décalage horaire (par rapport à l’UTC) d’une date quel que soit son fuseau horaire en sélectionnant la bonne période (par parcours du tableau zone et comparaison avec les propriétés from et to).

Nous verrons dans un prochain article comment mettre en oeuvre cette possibilité.

Construire des dates exprimées en UTC via Javascript

Le constructeur Date comporte plusieurs formes permettant de créer une instance de Date :

new Date() donne la date courante (dans l'horaire du poste dans lequel le navigateur est lancé)

new Date(1974,02,10,19,20,30,461) permet de créer une date en lui spécifiant des paramètres exprimés dans la zone horaire du poste client.

Dans tous les cas, la forme imprimable de la date (le toString) est exprimé dans la zone horaire du poste client dans lequel le navigateur est lancé.

En fait,  La seule forme du constructeur Date permettant de créer une date en spécifiant des arguments exprimés en UTC est de passer un nombre de millisecondes au constructeur, par exemple :

new Date(1268211600000)

En javascript, l'affichage standard des dates est toujours exprimé en temps local (par exemple : Wed Mar 10 2010 10:00:00 GMT+0100) et s'il est possible de récupérer le décalage d'une date par rapport à l'UTC (via getTimezoneOffset), il n'est pas possible de modifier le timeZoneOffset directement.

Autrement dit, il n'existe pas nativement en javascript de "date locale" ou "date UTC" ; il existe juste la capacité de créer des dates en spécifiant des arguments dans la zone horaire du poste client ou en UTC. Une fois la date créée, il n'existe plus de moyen de savoir si elle a été initialement créée dans la zone horaire du poste client ou en UTC.

Il existe cependant plusieurs méthodes pour construire des dates javascript en utilisant des informations renseignées en UTC.

Méthode 1 : compter en millisecondes 🙂

On passe au constructeur Date un nombre de millisecondes (depuis le 1er Janvier 1970), ce qui permet de préciser un temps UTC (en millisecondes) qui sera convertie en une instance de Date en heure locale.

Par exemple,

new Date(1268211600000) Wed Mar 10 2010 10:00:00 GMT+0100

Méthode 2 : utiliser Date.UTC

La manipulation de millisecondes (par exemple la sauvegarde en base de données du nombre de millisecondes) n’est pas très pratique. La méthode Date.UTC permet de calculer le nombre de millisecondes pour une date en UTC en précisant les mêmes arguments que le constructeur Date, c'est-à-dire de 2 à 7 arguments, respectivement year, month, date, hour, minutes, seconds, milliseconds

Par exemple

Date.UTC(2010,02,10,9,0) 1268211600000

Pour créer (en temps local) une Date en précisant l’heure UTC, on peut enchaîner les deux appels :

new Date(Date.UTC(2010,02,10,9,0))Wed Mar 10 2010 10:00:00 GMT+0100

Méthode 3 : utiliser les accesseurs de type setUTCXXX

Une troisième solution est de créer une instance de Date et d’enchaîner les accesseurs du type setUTCXXX (setUTCDate, setUTCFullYear,setUTCHours, setUTCMinutes, setUTCMonth, setUTCSeconds)

Méthode 4 : convertir une chaîne au format ISO 8601 UTC

La méthode javascript Date.parse permet de transformer une chaîne en nombre de ms depuis 1970 (temps UTC).

Par exemple,

Date.parse("Jul 8, 2005") 1120773600000

malheureusement, cette méthode ne permet pas de parser des chaînes au format ISO 8601, qui sont pourtant le standard internationnal reconnu d'échange de dates (il est intéressant d'utiliser ce standard parce qu'il permet l'échange avec les autres langages informatiques).

La représentation complète des Dates en ISO 8601

ISO 8601 définit de nombreux formats différents pour échanger les heures, les dates, et même les périodes de temps et les événements récurrents.

Parmi ces formats, ceux qui nous intéressent sont ceux qui permettent de convertir sous forme de chaîne l'intégralité d'une date javascript. Or les dates javascript on une précision de l'ordre de la milliseconde. Le format ISO 8601 à utiliser doit donc pouvoir stocker "la date complète calendaire et l’heure avec des fractions de seconde"

ISO 8601 définit les formats correspondants suivants :

le format étendu : "aaaa-mm-qqThh:mi:ss,nzzzzzz" (ex : 1997-07-16T19:20:30,4+01:00)

et le format de base (qui est le même mais sans les tirets) : "aaaammqqThhmissnzzzzz" (ex : 19970716T1920304+0100)

et en UTC :

le format étendu : "aaaa-mm-qqThh:mi:ss,nZ" (ex : 1997-07-16T19:20:30,4Z)

et le format de base (qui est le même mais sans les tirets) : "aaaammqqThhmissnZ" (ex : 19970716T1920304Z)

avec

  • aaaa : l'année sur 4 chiffres, mm : le mois sur 2 chiffres, qq : le quantième (index du jour) sur 2 chiffres,
  • T est le séparateur date/heure
  • ss : les secondes précisées par 2 chiffres,
  • n : les fractions de secondes - sur un nombre de chiffres quelconques - indiquées après la virgule
  • zzzzzz le fuseau horaire dans le format étendu (+hh:mi ou -hh:mi)
  • zzzzz le fuseau horaire dans le format de base (+hhmi ou hhmi)

En ISO 8601, le T indique le début du codage de l'heure.

Un Z final indique le méridien Zéro ce qui indique qu'il s'agit d'une heure UTC.

En heure locale, on indique le fuseau horaire : sous un format de base (+hh, +hhmm ou -hhmm) ou un format étendu (+hh:mm ou -hh:mm), par exemple :

aaaammqqThh:mizzzzzz (ex : 1997−07−16T19:20+01:00)

Noter qu'en ISO 8601, il est possible d'indiquer des fractions de secondes (en utilisant officiellement la virgule mais les anglo-saxons continuent à favoriser l'utilisation du point) et que la précision en terme de fractions de secondes n'est pas limitée.

par exemple :

1974−03−10T19:20:30,561Z correspond au 10 Mars 1974 à 19h20, 30 secondes et 561 ms (tout cela en UTC)

En utilisant une chaîne ISO 8601 et en précisant les millisecondes, on peut donc conserver la même précision qu'en javascript.

Générer une chaîne ISO 8601 UTC en javascript

Il est facile de génerer une chaîne UTC.  La méthode toISOString supportée par Firefox joue ce rôle (noter par contre, que la méthode javscript  toUTCString n’utilise pas ce format, mais un format tel que Tue, 04 May 2010 08:07:10 GMT).

var d0=new Date(Date.UTC("1974",02,10,19,20,30,461))
console.log(d0.toISOString())  
1974-03-10T18:20:30.461Z

On peut aussi utiliser la méthode toJSON présentée ci-dessous. 

JSON supporte nativement la génération de chaînes ISO 8601 au format UTC, via la méthode toJSON et conserve la précision de la milliseconde.

var d1=new Date("1974",02,10,19,20,30,461) ; // Date locale
var utcTime=d1.getTime(); // traduite en UTC (nombre de millisecondes)
console.log("utcTime : "+utcTime)
132171630461

var d2=new Date(utcTime+561); // on ajoute 561 ms

console.log(d2);
→ Sun Mar 10  1974 19:20:30 GMT+0100

console.log(d2.toJSON());

→ 1974-03-10T18:20:30.561Z

On trouve un exemple de code permettant de générer une chaîne ISO 8601 UTC associé ici :

Parser une chaîne ISO 8601 UTC en javascript

Noter que l'implantation de référence JSON (http://www.json.org/json2.js) comporte un exemple d'analyse de chaîne ISO 8601.

var myData = JSON.parse(text, function (key, value) {
 var a;
 if (typeof value === 'string') {
 a = /^(d{4})-(d{2})-(d{2})T(d{2}):(d{2}):(d{2}(?:.d*)?)Z$/.exec(value);
 if (a) {
 return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
 +a[5], +a[6]));
 }
 }
 return value;
}

Mais attention, ce code de référence ne respecte pas la précision des millisecondes (il ne comporte que 6 paramètres, alors que c'est le 7ième qui  précise les secondes) ; ainsi dans le code de référence, JSONParseDate("1974-03-10T18:20:30.561Z").getTime() rend 132171630000 (et pas 132171630461)

Il faut donc corriger ce code pour tenir compte des millisecondes, ce qui donne par exemple  :

/**
 * Parse une chaîne conforme aux masques tels que aaaa-mm-qqThh:mi:ss,nzzzzz ou aaaa-mm-qqThh:mi:ss,nZ
 * Rends une Date ou NaN si cette chaîne n'est pas conforme aux masques
 * @param isoDate <String> une chaîne représentant une date
 *@return <Date | NaN>
 */
function ParseDate(isoDate) {
 var a;

 a = /(d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:.(d+))*(?:(?:([+|-])(0[0-9]|1[0-2]):(00|15|30|45)|Z))$/.exec(isoDate);
 if (a) {
 var year=a[1];
 var month=a[2];
 var day=a[3];
 var hour=a[4];
 var mm=a[5];
 var ss=a[6];
 var zz=a[7]; // fractions de secondes (a priori pas limitées en précisions) ou undefined
 var tmz_sign=a[8]; // "+", "-" ou undefined (si Z comme UTC)
 var tmz_hh=+a[9]; // l'heure de décalage par rapport à l'UTC
 var tmz_mm=+a[10]; // les minutes de décalage par rapport à l'UTC (volontairement limitées au quart d'heure (00|15|30|45)

 if(mm==undefined) { mm=0 };
 if(ss==undefined) { ss=0 };
 if(zz==undefined) { zz=0 };

 if(tmz_sign==undefined) {
 // On est dans le cas UTC
 // Création directe de la date en UTC
 var utcTime=new Date(Date.UTC(+year, +month - 1, +day, +hour, +mm, +zz));
 return utcTime
 } else {
 // On est dans le cas GMT
 // Création de la date en UTC (sans tenir compte du décalage horaire)
 var utcDateWithoutTZ=new Date(Date.UTC(+year, +month - 1, +day, +hour, +mm, +ss,+zz));
 // Conversion de cette date en UTC en millisecondes
 var ms=utcDateWithoutTZ.getTime();
 // Conversion du décalage horaire en ms
 var tmz_ms=((+tmz_hh*60) + tmz_mm)*60*1000;
 // Calcul du signe du décalage horaire vis à vis de l'UTC,
 // ajout au nombre de ms de la date UTC
 // Création de la date en lui spécifiant le nombre de ms
 var gmtTime;
 if(tmz_sign=="+")
 { gmtTime=new Date(ms-tmz_ms) }
 else
 { gmtTime=new Date(ms+tmz_ms) };
 return(gmtTime);
 }
 } else {
 return(NaN)
 }
}

Un code pour parser une chaîne ISO 8601 UTC est disponible en  http://delete.me.uk/2005/03/iso8601.html

Conclusion

En utilisant le format complet ISO 8601, il est possible d'avoir la même précision sous forme de chaîne qu'une date javascript et on peut facilement échanger ces dates avec d'autres systèmes, par exemple avec un serveur.

Néanmoins, il est intéressant de noter que JSON Schema (dont la version de Mars 2010 est disponible ) différencie plusieurs formes de types liés à la gestion du temps :

  • date-time - This should be a date in ISO 8601 format of YYYY-MM-DDThh:mm:ssZ in UTC time.  This is the recommended form of date/timestamp.
  • date - This should be a date in the format of YYYY-MM-DD.  It is recommended that you use the "date-time" format instead of "date" unless you need to transfer only the date part.
  • time - This should be a time in the format of hh:mm:ss.  It is recommended that you use the "date-time" format instead of "time" unless you need to transfer only the time part.
  • utc-millisec - This should be the difference, measured in milliseconds, between the specified time and midnight, January 1, 1970 UTC.  The value should be a number (integer or float).

Si on oublie les formats date et time qui ne supportent que des types partiels pas recommandés, JSON Schema différencie utc-millisec (qui correspond au nombres de millisecondes depuis le 1er Janvier 2010 et donc bien à l'API javascript getTime) et date-time qui ne comporte pas les fractions de secondes (mais qui est exprimé sous forme de chaîne ISO 8601). Autrement dit, dans la version actuelle de la norme, si on veut échanger avec la précision de la milliseconde, il faut utiliser le type utc-millisec. Si on en reste à la précision de la seconde, on peut utiliser une chaîne UTC et le type date-time.

Gestion javascript de l’heure d’été et de l’heure d’hiver

Les changements saisonniers d’horaires (heure d’été/heure d’hiver) causent déjà de nombreux maux de tête à nous autres pauvres humains. Mais la gestion par l'informatique de ces changements d'horaires est encore plus problématique.

Cet article a pour objet d'éclaircir cette problématique dans le cadre de l'implantation javascript des Dates. Pour favoriser vos propres recherches sur internet, noter qu’en anglais, cette notion s’appelle daylight savings time (DST).

Comme les problèmes informatiques liés aux changements d'horaires sont fréquents, il est logique qu'une base centralisée ait été mise en place sur ce sujet. Cette base de données collaborative est appelée tz database (ou « Olson Database »). Elle contient les informations liées aux fuseaux horaires et comment sont gèrés l’heure saisonnière. On trouve également de nombreuses informations afférentes intéressantes sur http://twinsun.com/tz/tz-link.htm

1. La création de Date javascript en heure locale

Dans son implantation de la classe Date, Javascript gère automatiquement les passages à l’heure d’été ou à l’heure d’hiver.

En effet, lorsqu’on utilise le constructeur Date, comme dans new Date(2010,2,28,1,59,0) , le constructeur crée une instance de Date dans le fuseau horaire du navigateur et tient compte du changement d'heure. (mais on ne peut pas préciser le décalage horaire par rapport à UTC, on peut juste le calculer via getTimezoneOffset())

En ce qui concerne la France, une directive européenne précise que :

  • la période de l’heure d’été commence, à 1 heure du matin, UTC, le dernier dimanche de mars ;
  • la période de l’heure d’été se termine, à 1 heure du matin, UTC, le dernier dimanche d’octobre.

En Javascript, on peut vérifier qu’une date créée en heure d’hiver possède 1 heure de décalage par rapport au temps UTC :

new Date(2010,2,28) → Sun Mar 28 2010 00:00:00 GMT+0100

alors qu’une date créée le lendemain (après le passage à l’heure d’été) possède 2 heures de décalage vis à vis du temps UTC :

new Date(2010,2,29) → Mon Mar 29 2010 00:00:00 GMT+0200
On peut aussi utiliser la méthode toUTCString pour afficher la date en temps UTC.

2. Le calcul de l'offset par rapport au temps universel

L’API getTimezoneOffset() rend en minutes le décalage du fuseau horaire de la date considérée par rapport à l’UTC, ainsi :

une date en heure d’été, donc en GMT+0100, a un getTimezoneOffset() qui rend -60

une date en heure d’hiver, donc GMT+0200, a un getTimezoneOffset() qui rend -120

Donc en heure locale française (GMT+0100) :

(new Date(2010,2,28)).getTimezoneOffset() → -60

(new Date(2010,2,30)).getTimezoneOffset() → -120

3. L'influence des paramètres de l'operating system

Le fuseau horaire du navigateur est définit par l’operating system dans lequel le navigateur tourne. Par exemple, sous Windows, si vous modifiez l’horloge Windows pour changer le fuseau horaire, la date locale que vous créez via javascript dans votre navigateur ne correspondra pas à la même heure en temps UTC (temps universel coordonné).

On peut faire l’expérience par exemple sous Firefox (noter qu’il n’est pas nécessaire de relancer le navigateur, un simple rechargement de la page suffit).

Si on change le fuseau horaire Windows en le passant en GMT+05:45 (fuseau horaire de Katmandou), on a, après rechargement de la page html :

(new Date(2010,2,28)).getTimezoneOffset() → -345

(new Date(2010,2,30)).getTimezoneOffset() → -345

Ce résultat laisse entendre qu’il n’y pas de changement d’horaire saisonnier à Katmandou (ou en tout cas pas à la même période).

4. Les heures litigieuses (passage d'heure d'hiver à heure d'été)

On peut vérifier qu'en heure locale de France métropolitaine, le changement d’heure d'hiver à heure d'été s’effectue bien à 2h du matin.

À 1h59 du matin, heure locale (donc 0h59 UTC) :

new Date(2010,2,28,1,59,0) → Sun Mar 28 2010 01:59:00 GMT+0100

via toUTCString(), on obtient : Sun Mar 28 2010 00:59:00 GMT

À 3h00 du matin, heure locale (donc 1h00 UTC) :

new Date(2010,2,28,3,0,0) → Sun Mar 28 2010 03:00:00 GMT+0200

via toUTCString(), on obtient : Mon Mar 29 2010 1:00:00 GMT

Mais attention ! les heures locales entre 2h00 et 2h59 (qui n’existent pas officiellement), sont mappées par javascript. Ainsi 2h59, heure locale donne :

new Date(2010,2,28,2,59,0) → Sun Mar 28 2010 01:59:00 GMT+0100

ce qui correspond en fait à la même heure que 1h59.

new Date(2010,2,28,1,59,0) → Sun Mar 28 2010 01:59:00 GMT+0100

Nous verrons dans un futur article comment gérer de façon générale la validité des dates (l’équivalent du mode “lenient” de java.

5. Les heures litigieuses (passage d'heure d'hiver à heure d'été)

En ce qui concerne le passage de l'heure d'été à l'heure d'hiver, il s'effectue  bien à  1h du matin UTC.

À 1h00 du matin, heure locale (donc 23h00 UTC) :

new Date(2010,9,31,0,0) → Sun Oct 31 2010 01:00:00 GMT+0200

À 1h59 du matin, heure locale (donc 23h59 UTC) :

new Date(2010,9,31,1,59) → Sun Oct 31 2010 01:59:00 GMT+0200

À 2h00 du matin, heure locale (donc 1h00 UTC) :

new Date(2010,9,31,2,00) → Sun Oct 31 2010 02:00:00 GMT+0100

En fait, javascript a fait un choix. En effet, en réalité, à 3heures du matin (heure d'été), nous reculons nos montres pour qu'elles marquent 2h du matin (heure d'hiver). Autrement dit, nos montres indiquent deux fois de suites les heures de 2h00 à 2h59... une première fois, il s'agit des heures UTC de 0h00 à 0h59, puis une fois nos montres reculées, il s'agit des heures UTC de 1h00 à 1h59.

Donc quand on indique en heures locales, new Date(2010,9,31,2,00), javascript fait le choix d'interpréter cela comme une heure d'hiver (et donc comme 1h00  UTC) : on a bien Sun 31 Oct 2010 01:00:00 GMT.

Il faut noter que de cette façon, il n'est pas possible d'obtenir les conversions vers les heures UTC de 0:00 à 0:59:59 mais elles n'ont pas disparues ! On peut les obtenir :

  • en convertissant cette date en UTC et en lui retirant par exemple 2 secondes (2000 millisecondes). On obtient alors Sun Oct 31 2010 02:59:58 GMT+0200 (et en UTC : Sun, 31 Oct 2010 00:59:58 GMT),
  • L'autre solution est d'utiliser Date.UTC qui permet de créer directement une heure UTC. Cela donne: new Date(Date.UTC(2010,09,31,0,59,58));

Tout cela milite pour la manipulation de dates UTC, surtout lors du changement d'horaire !

Conclusion

Quand on exécute un new Date(), la date résultante dépend de l’heure rendue par l’horloge du système d’exploitation et bien entendu, le fuseau horaire de ce même système d’exploitation.

Sous firefox windows :

  • le changement d’heure du système d’exploitation est immédiatement répercuté sur le résultat donné par new Date() – sans rechargement de la page html –
  • le changement du fuseau horaire du système d’exploitation n’est répercuté qu’avec rechargement de la page html

Lorsqu’on crée une date complète (comme new Date(2010,3,10,9,0,0), Javascript semble gérer correctement le passage de l’heure d’hiver à l’heure d’été (tant que l’on ne crée pas explicitement des dates situées entre 2 et 3heures du matin le jour du changement de l'heure d'hiver à l'heure d'été).

Noter cependant que la date rendue :

  • est calculée dans le fuseau horaire du navigateur au moment de l’exécution de ce code
  • ne dépend pas de l’heure de l’horloge du navigateur
  • ne dépend pas de l’heure d’été ou d’hiver au moment de l’exécution de ce code, mais juste du fuseau horaire.

Sa conversion en UTC dépend donc quand même du système d’exploitation où tourne le navigateur.

Il ne semble pas possible d’obtenir directement via Javascript le fuseau horaire, on ne peut obtenir que l’offset d’une date donnée par rapport au temps UTC (via getTimezoneOffset comme on l’a montré ci-dessus).

Dans un prochain article, nous aborderons les problèmes spécifiques posés par les changements d’heures saisonnier quand on souhaite gérer des événements récurrents.