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.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.