Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

La bibliothèque Noda Time

Poster un commentaire

.Net apporte un support relativement conséquent aux dates et durées. En première ligne nous rencontrons DateTime et TimeSpan qui gèrent respectivement une date/heure et une durée. On peut également prendre en charge des calendriers avec Calendar et les fuseaux horaires avec TimeZoneInfo.

 

On pourrait croire le support plutôt complet, qu’ajouter d’autres? Et pourtant dès que l’on y pense un peu, les problèmes surgissent assez vite. Par exemple:

  • Quelle date retourne exactement DateTime.Now? Est-ce la même sur tous les postes de travail?
  • Examinez le code suivant:
DateTime dt1 = ...;
DateTime dt2 = ...;
TimeSpan ts = dt1 - dt2;
  • Qui vous prouve que cette opération a un sens? Les dates sont t’elles par exemple dans le même fuseau? Et pourtant aucune alerte ni exception ne sera levée.
  • La propriété de type DateTime de telle fonction attend t’elle une date UTC ou autre choses?
  • Savez vous écrire facilement un code qui permet de transformer une date en sa valeur dans un autre fuseau horaire?
    Note: vous trouverez une début de réponse ici:
    https://amethyste16.wordpress.com/2013/05/24/la-question-des-fuseaux-horaires/
  • Est-ce facile d’écrire un tests unitaire dont le résultat dépend d’une heure données?

 

Et bien d’autres problèmes encore:

http://blog.nodatime.org/2011/08/what-wrong-with-datetime-anyway.html

 

Vous connaissez la réponse, rien de tout cela est facile. La bibliothèque .Net n’apporte pas de support natif à ces problèmes.

C’est ici qu’intervient la bibliothèque Dota Time. Il s’agit de la version .Net de la librairie Java Joda Time.

Vous trouverez son site officiel ici:

http://nodatime.org/

La philosophie de cette librairie est de vous forcer à réfléchir sur la nature des éléments de temps que votre code manipule et surtout: rien de doit être implicite. Le code doit décrire avec précision ses intentions.

Je vous propose un petit voyage à travers cette librairie.

Je vais parler code et librairie. Donc ne vous contentez pas de lire ce qui suit, c’est inutile. Expérimentez. Vous aurez besoin d’une application Console et d’ajouter le package Nuget:

install-package NodatTime

J’ai fait mes essais avec la version 1.3, mais la 2.0 est déjà annoncée et posera des problèmes de compatibilité.

Premiers pas

Noda introduit un nouveau concept: l’Instant.

Un instant définit un moment dans le temps indépendamment de toute notion de fuseau horaire. Le DateTime auquel nous sommes habitué est la combinaisons d’un Instant et d’un fuseau horaire qui nous donne une date et heure.

Regardons un exemple:

Typiquement nous obtenons une date et heure de la façon suivante:

DateTime dt = DateTime.Now;

Avec Noda on ferait ceci:

Instant maintenant = SystemClock.Instance.Now;
DateTimeZone fuseau = DateTimeZoneProviders.Bcl.GetSystemDefault();
ZonedDateTime now = maintenant.InZone(fuseau);
Console.WriteLine(now);
Console.WriteLine(now.ToString("dddd dd MMMM YYYY", CultureInfo.CurrentCulture));

Qui affiche:

2014-09-26_21-24-14

Ce n’est pas la syntaxe qui est intéressante, mais la démarche:

  1. Ligne 1: j’instancie une classe Instant
  2. Ligne 2: On récupère le fuseau horaire du système
  3. Ligne 3: Cette fois on combine Instant et fuseau afin de créer une date et heure affichable

Les choses semblent plus complexes, mais ce n’est qu’apparence. DateTime tente de pousser sous le tapis ce qui peut poser problème. Avec Noda tout est clair, pas d’ambiguïtés. Vous devez réfléchir à la signification d’une date au moment où elle est créée, pas tout au long du code.

 

Autre exemple extrait de la documentation qui va nous familiariser avec quelques techniques de base:

Instant now = SystemClock.Instance.Now;

// Conversion d'un Instant en ZonedDateTime UTC
ZonedDateTime nowInIsoUtc = now.InUtc();

// Création d'une Duration
Duration duration = Duration.FromMinutes(3);

// On l'ajoute à nowInIsoUtc
ZonedDateTime thenInIsoUtc = nowInIsoUtc + duration;

// Obtient un DateTimeZoneProvider
DateTimeZone london = DateTimeZoneProviders.Tzdb["Europe/London"];

// Création d'une date locale
LocalDateTime localDate = new LocalDateTime(2012, 3, 27, 0, 45, 00);

// On la rattache à un DateTimeZone pour créer un ZonedDateTime
ZonedDateTime before = london.AtStrictly(localDate);

 

Dont le résultat donne:

2014-09-26_22-20-34

Les différents types que nous venons de découvrir ont des équivalent plus ou moins exact en .Net que nous résumons dans ce tableau:

Noda Equivalent .Net
Instant DateTime avec une propriété .Kind = DateTimeKind.Utc
ZonedDateTime DateTimeOffset + TimeZoneInfo
LocalDate DateTime avec une propriété .Kind = DateTimeKind.Unspecified (on ignore la portion de temps)
LocalTime DateTime avec une propriété .Kind = DateTimeKind.Unspecified (on ignore la portion de date)
LocalDateTime DateTime avec une propriété .Kind = DateTimeKind.Unspecified
Duration TimeSpan
DateTimeZone TimeZoneInfo

Noda gère donc les dates par rapport à un contexte fixe, UTC. N’importe quel autre fuseau aurait pu convenir, mais celui-ci est neutre. Cela n’a l’air de rien, mais les dates sont des données sensibles et  de la politique internationale se mêle à ce  genre de chose. Il y a quelques années Microsoft a été contraint de supprimer de Windows 95 une carte qui permettait de sélectionner de façon graphique son fuseau horaire.

 

Noda développe un concept important que l’on n’a pas en .Net: la notion de type global et local.

Un type local n’a pas de fuseau horaire associé. Sa valeur peut donc avoir un sens différent selon différentes personnes de part le monde. Il s’agit par exemple de LocalTime. Un type global se voit affublé d’un fuseau. Sa valeur est universelle. C’est par exemple ZonedDateTime.

La librairie Noda sépare clairement ces deux catégories de type qui ne peuvent jamais se combiner de façon ambiguë ou accidentelle.

Dès que le nom du type démarre par Local, on a affaire à un type local. Remarquons aussi que Noda définit pas moins de 3 types pour définir une date et heures locale: LocalDate, LocalTime, LocalDateTime. Ils n’ont pas d’équivalent direct en .Net. Mais on constate cette fois encore la volonté d’avoir des typages clairs, quitte à définir plus de types.

 

L’arithmétique de Noda

En général on a besoin de manipuler des dates, des décalages dans le temps… Comment les choses se passent dans Noda?

 

Noda manipule également des périodes et des décalages dans le temps à l’aide de 4 types:

Noda .Net
Period Une période dans un calendrier. Par exemple 6 mois
Offset Différence entre un temps local et UTC
OffsetDateTime Equivalent d’un DateTimeOffset. Mesure un instant par rapport à un décalage fixe
Duration TimeSpan

Le type Duration est ce qu’il y a de plus proche d’un TimeSpan. Il représente une durée dans le temps.

LocalDate date = new LocalDate(2014, 9, 27);
LocalDate dateOneMonthLater = date.PlusMonths(1);
LocalDate dateOneDaySooner1 = date.PlusDays(-1);

Period period = Period.Between(date,dateOneDaySooner1);
LocalDate dateOneDaySooner2 = date.Minus(period);

LocalTime time = new LocalTime(7, 15, 0);
time = time.PlusHours(3);

LocalDateTime dateTime = date + time;
dateTime = dateTime.PlusWeeks(1);

 

L’exécution nous donne:

2014-09-27_15-41-34

Observez la façon dont on a construit un LocalDatetime à partir d’un LocalDate et d’un LocalTime.

Period et Duration

2014-09-27_19-41-39

Il n’est peut être pas inutile de préciser un peu plus ces deux types qui se ressemblent beaucoup. Duration représente une durée exprimée en ms, tandis que Period est une durée en termes d’expressions comme: année, mois, jours… (par exemple 2 ans, 8 mois, 5jours et  13 heures).

Il existe de nombreuses façon de les construire. Nous en avons vu quelques unes, mais on pourrait aussi ajouter ces méthodes utilitaires:

Period periode = Period.FromYears(5); // période de 5 ans
Duration duration = Duration.FromMinutes(10); // durée de 10 minutes

 

On peut ajouter une Period à un LocalDate ou un LocalTime, mais à condition de respecter la nature du type. Par exemple ces deux opérations soulève un ArgumentException:

LocalTime localTime = new LocalTime(1, 0, 0); // 1 heure
Period period1 = Period.FromYears(1);
LocalTime add1 = localTime + period1;

LocalDate localDate = new LocalDate(1, 0, 0); // 1 an
Period period2 = Period.FromMinutes(1);
LocalDate add2 = localDate + period2;

Un LocalTime ne peut en effet contenir d’informations de date et de même un LocalDate ne contient pas d’informations d’heure. Vous devez choisir un LocalDateTime.

Par contre deux Period peuvent se combiner sans problèmes.

On peut créer ainsi une Period de 2 ans, 8 mois, 10 jours et 4 heures ainsi:

Period p = Period.FromYears(2) + Period.FromMonths(8) + Period.FromDays(10) + Period.FromHours(4);

C’est un exemple de la protection qu’apporte Noda aux données.

 

Les fournisseurs de fuseau

Vous avez peut-être remarqué dans les exemples qui précèdent que l’on obtenait un fuseau soit depuis un type Bcl ou Tzdb.

Les fuseaux horaires sont une chose complexe, pleine de choses très bizarres. Il n’est pas possible à l’échelle d’un développeur de les gérer lui-même, il doit faire appel à des bibliothèques spécialisées. Il en existe actuellement deux:

  • La Bcl proposée par Microsoft
  • La Tzdb proposée par l’IANA

Noda gère les deux, mais la librairie de l’IANA est la plus complète.

 

Bibliographie

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s