Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Prochaine sessions inter-entreprises : 13-16 février 2018
Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Gagnez du temps avec Joda Time !

La représentation et la manipulation des dates et heures est un point notoirement faible de Java depuis sa version 1.0 - et je ne vous parle pas des Timezones...
Il existe heureusement une petite librairie fort pratique pour combler ce vide sidérant : Joda Time. Développée par Stephen Colebourne, elle est en passe d'être standardisée au sein du JDK sous la JSR-310 ("Date & Time API").

Si vous ne la connaissez pas encore, suivez le guide !

Les classes du JDK

AUjourd'hui, les dates sont représentées grâce à 2 classes :

  • java.util.Date, qui représente un point fixe dans le temps, calculé sous la forme du nombre de millisecondes écoulées depuis le 01/01/1970 GMT ("Epoch").
    La majorité de ses constructeurs (4/6) et méthodes (18/22) sont déclarés obsolètes car buggés...
  • java.util.Calendar, qui prend en compte différents types de calendriers (grégorien, impérial Japonais...), mais n'autorise que certaines manipulations basiques (ajout ou retrait d'heures, jours...) au travers d'API pas toujours très cohérentes.

L'API standard est donc très pauvre, et ne permet pas une grande fantaisie dans la manipulation des dates et heures.

Joda Time

Joda Time fournit heureusement une API bien plus riche, répondant aux besoins concrets rencontrés en entreprise.

Toute la librairie est architecturée autour de 5 concepts :

  • Instant : Date et heure complètes : 25/12/2012 00:00:000 (avec timezone)
  • Partial : Date et/ou heure incomplètes, "flottantes" : 25/12 (Noël), 12:00 (midi)
  • Interval : Intervalle entre des Instants de début et de fin
  • Duration : Durée précise, en millisecondes
  • Period : Représentation "humaine" d'une durée : 1 mois (qu'il ait 28 ou 31 jours)

Voyons-les en détail ; puis je vous parlerai de l'astucieuse représentation interne des champs au sein de ces classes.


Instant

Le concept d'Instant est représenté par l'interface ReadableInstant, implémenté par les classes

  • Instant : Timestamp universel, indépendant des timezones (immuable). C'est l'équivalent de la classe java.util.Date.
  • DateTime : Timestamp dépendant d'une timezone (immuable).
  • DateMidnight : DateTime dont l'heure est à 00:00:000 (immuable). Pratique lorsqu'on raisonne en termes de jours/mois plutôt que de minutes/heures.
  • MutableDataTime : Timestamp dépendant d'une timezone (modifiable)


instant.png

Un petit exemple de mise en oeuvre :

// Instant courant universel
Instant now = Instant.now();
long millisSinceEpoch = now.getMillis();
 
// Ajouter une heure à une date JDK
Date jdkDate = new Date();
MutableDateTime mdt = new MutableDateTime(jdkDate);
mdt.addHours(1);
jdkDate = mdt.toDate();
 
// Trouver le jour de la semaine
DateTime dt = now.toDateTime();
int day = dt.getDayOfWeek();  // DateTimeConstants.MONDAY
DateTime.Property dowp = dt.dayOfWeek();
String dayName = dowp.getAsText(Locale.FRENCH);  // lundi
 
// Positionner au dernier jour de la même semaine
dt = dayOfWeekProperty.withMaximumValue();
dayName = dt.dayOfWeek().getAsText(Locale.FRENCH);  // dimanche


Partial

Un Partial est une réprésentation partielle ou incomplète d'une date. Il n'est en effet pas toujours indispensable de disposer d'une précision à la milliseconde, ni de préciser une timezone.

Le concept est représenté par l'interface ReadablePartial, et implémenté par :

  • LocalDate, LocalTime, LocalDateTime, YearMonth et MonthDay (immuables), qui permettent de stocker les combinaisons les plus courantes d'heures, jours, mois ou années.
  • Partial (immuable) qui permet de définir ses propres compositions de champs.

Pour retransformer un Partial en DateTime (date complète), il suffit de compléter les champs manquants et de préciser une timezone lors de l'appel de la méthode toDateTime().


partial.png

Partial en action :

// Tea time à 17:00
LocalTime teaTime = new LocalTime(17, 0);
DateTime teaTimeToday = teaTime.toDateTime(Instant.now());
System.out.println(teaTimeToday); // 2012-08-24T17:00:00.000Z
 
// Noël le 25 décembre
LocalDate christmas = new LocalDate(2012,12,25);
 
// Noël en TZ UTC : 25/12/12 00:00:00 UTC
DateTime utcChristmas =
  christmas.toDateTimeAtStartOfDay(DateTimeZone.UTC);
 
// Noël à Paris : 25/12/12 00:00:00 Europe/Paris
//              = 24/12/12 23:00:00 UTC
DateTimeZone paris = DateTimeZone.forID("Europe/Paris");
DateTime localChristmas = 
  christmas.toDateTimeAtStartOfDay(paris);


Interval

Un Interval représente un segment de temps entre deux Instants, c'est-à-dire deux dates précides. Un Interval n'est donc pas librement transposable (voir Duration et Period plus bas).

Modélisé par l'interface ReadableInterval, il est implémenté par :

  • Interval (immuable)
  • MutableInterval (modifiable)

On le construit en précisant les deux Instants définissant ses bornes, ou à l'aide d'un Instant initial et d'une Duration.
On peut alors déterminer s'il contient un Instant donné, ou est avant / après / pendant / recouvre un autre intervalle, et mesurer sa durée précise en millisecondes (conversion en Duration) ou sa durée "conceptuelle" (conversion en Period).


interval.png

Un petit exemple de circonstance :

Instant winter = new DateTime(2012,12,21,0,0,0,0).toInstant();
Instant spring = new DateTime(2013, 3,21,0,0,0,0).toInstant();
Interval skiTime = new Interval(winter, spring);
 
// Test de la Saint-Valentin
Instant loversDay = new DateTime(2013,2,14,0,0,0,0).toInstant();
System.out.println(skiTime.contains(loversDay));  // true
 
// Durée exacte : 7776000 secondes
Duration duration = skiTime.toDuration(); 
System.out.println(duration);			// PT7776000S
 
// Période conceptuelle : 3 mois
Period period = skiTime.toPeriod(); 
System.out.println(period);	// P3M


Duration

Quatrième concept, très simple : Duration, qui représente une durée en millisecondes, souvent récupérée à partir d'un Interval.

Modélisé par ReadableDuration, il n'a qu'une implémentation :

  • Duration (immuable)


duration.png

// Âge en jours
Instant birthDate = (...)
Instant now  = Instant.now();
Duration age = new Duration(birthDate, now);
int days = age.toStandardDays().getDays();
 
// Dans une heure
Duration oneHour = Duration.standardHours(1);
Instant inOneHour = now.plus(oneHour);
 
// Dans une minute
Duration oneMinute = Duration.parse("PT60.0S");  // ISO8601
Instant inOneMinute = now.plus(oneMinute);
 
// Durée de la période de ski
Instant winter = new DateTime(2012,12,21,0,0,0,0).toInstant();
Instant spring = new DateTime(2013, 3,21,0,0,0,0).toInstant();
Interval skiTime = new Interval(winter, spring);
Duration skiTimeDuration = skiTime.toDuration();


Period

Et pour terminer, voici le concept de Period, qui permet de manipuler des durées "flottantes", "conceptuelles" ou "humaines". Oui, cela fait beaucoup de guillemets, mais vous allez comprendre pourquoi.

Une Period ne représente pas une durée de longueur fixe et bien définie entre deux points dans le temps, mais une durée conceptuelle "flottante", de durée réelle variable (ex: un jour, un mois, un an...). Cela permet d'effectuer des calculs assez naturels comme "ajouter un mois", indépendamment du nombre de jours réel par mois (15 janvier + 1 mois = 15 février, et 15 février + 1 mois = 15 mars).

L'interface ReadablePeriod est implémentée par :

  • Years, Months, Weeks, Days, Hours, Minutes, Seconds (immuables) : périodes exprimées sur une seule unité de temps.
  • Period (immuable) : Période multi-unités de temps
  • MutablePeriod (modifiable) : variante modifiable de Period.


period.png

// Âge en jours
Instant birthDate = (...)
Instant now = Instant.now();
int age = Days.daysBetween(birthDate, now).getDays();
 
// Âge en jours, via un Interval
Interval life = new Interval(birthDate, now);
Days daysInLife = Days.daysIn(life);
 
// Ajout d'un mois (quel que soit son nombre de jours)
ReadablePeriod oneMonth = Period.months(1);
DateTime inOneMonth = now.toDateTime().plus(oneMonth);


Représentation interne des champs

Si vous avez étudié attentivement les exemples de code précédents, vous avez sans doute remarqué que les classes proposent deux façons d'obtenir la valeur d'un champ particulier :

  • en utilisant une méthode de type get<Field>() (ex: getDayOfWeek()), qui en renvoie sa valeur numérique;
DateTime dt =;
int day = dt.getDayOfWeek();
  • ou via une méthode de type <field>() (ex: dayOfWeek()) qui renvoie un objet de type Property offrant de nombreuses fonctionnalités intéressantes.
DateTime dt =;
DateTime.Property dayProperty = dt.dayOfWeek();

Une Property permet d'obtenir des méta-informations sur un champ particulier, et de le manipuler.
Quelques exemples :

Instant now = Instant.now();
 
// Représentation textuelle
now.dayOfWeek().getAsText(Locale.FRENCH);   // mardi
now.monthOfYear().getAsText(Locale.FRENCH); // janvier
 
// Détermination des minimum/maximum courants et absolus
now.dayOfMonth().getMaximumValue();         // 31j en janvier
now.dayOfMonth().getMaximumValueOverall();  // 31j max / mois
 
// Mise au minimum/maximum courants
now = now.monthOfYear().withMaximumValue(); // en décembre


Manipulation des dates

Comme précédemment indiqué, la plupart des classes sont immuables ; toute opération de modification renvoie une nouvelle instance. C'est en général une bonne chose, car cela les rend automatiquement thread-safe, et facilite le raisonnement sur l'état de l'application. Toutefois, si vous devez enchaîner plusieurs opérations, il est préférable d'utiliser les variantes modifiables des classes (ex: DateTime -> MutableDateTime) pour éviter la création inutile d'objets intermédiaires.

// Avec DateTime
DateTime now = DateTime.now();
now = now.hourOfDay().addToCopy(4);    // nouvelle instance !
 
// Avec MutableDateTime
MutableDateTime now = MutableDateTime.now();
now.hourOfDay().add(4);                 // variante 1
now.addHours(4);                        // variante 2
now.add(DurationFieldType.hours(), 4);  // variante 3

Et pour vous montrer la puissance de cette API, voici quelques exemples un peu plus sophistiqués !

  • Calcul de la facturation à fin du mois + 90 jours :
MutableDateTime factu = MutableDateTime.now();
factu.dayOfMonth().set(factu.dayOfMonth().getMaximumValue());
factu.addDays(90);
  • Tous les vendredi 13 de l'année :
MutableDateTime day = new MutableDateTime();
day.setDayOfMonth(13);
for (int i = 1; i <= 12; i++) {
    day.setMonthOfYear(i);
    if (day.getDayOfWeek() == DateTimeConstants.FRIDAY) {
        String m = day.monthOfYear().getAsText(Locale.FRENCH);
        System.out.println("Vendredi 13 " + m);
    }
}

Inter-opérabilité JDK - Joda Time

Joda Time propose évidemment une certaine interopérabilité avec les classes standards du JDK.
En attendant l'adoption de la JSR-310, java.util.Date a encore de beaux jours devant elle, et continuera à hanter notre code encore longtemps.

La classe DateTime est le point de conversion le plus courant entre les deux univers.

DateTime accepte en paramètre de constructeur

  • Une java.util.Date
  • Un java.util.Calendar
  • Un java.util.GregorianCalendar
DateTime dt = new DateTime(new Date());
DateTime dt = new DateTime(Calendar.getInstance());
DateTime dt = new DateTime(new GregorianCalendar());

DateTime peut également faire le pont vers les classes du JDK

  • Vers java.util.Date
  • Vers java.util.Calendar
  • Vers java.util.GregorianCalendar
DateTime dt = DateTime.now();
 
Date d               = dt.toDate();
Calendar cal         = dt.toCalendar(Locale.FRENCH);
GregorianCalendar gc = dt.toGregorianCalendar();

Conclusion

Si vous ne connaissiez pas Joda Time, j'espère que cette présentation vous aura convaincu de l'ajouter à votre projet !

A la fois simple et puissante, cette petite librairie représente tout ce qu'auraient dû être java.util.Date et java.util.Calendar. Mais comme le dit Stephen Colebourne, personne ne s'intéresse aux problématiques de dates : ce n'est tout simplement pas sexy par rapport à d'autres aspects du langage (entre autre, les futures expressions lambda), et il est difficile de trouver des développeurs motivés pour mettre la dernière main à l'API et enfin l'intégrer dans le JDK.
On croise les doigts !


Commentaires

1. Le vendredi 1 février 2013, 17:52 par Brice

Pour info pour ce qui est des bugs et autres soucis, il y a une page plus précise sur le sujet ici : http://mestachs.wordpress.com/2012/...

2. Le lundi 4 février 2013, 23:12 par Alexis Hassler

Super synthèse de Joda Time. Le seul pont sur lequel je tiquerais, c'est quand tu dis que la librairie est simple. Elle est riche et puissante, mais pas simple.

La différence entre un Instant et un DateTime n'est pas évidente, ni celle entre un DateMidnight et un Partial. Et quand il faut choisir entre un Duration, un Period et un Interval, l'intuition ne suffit pas.

Je suis d'accord pour dire que c'est le prix à payer pour avoir une API riche, mais ce n'est pas simple.

3. Le jeudi 11 avril 2013, 08:09 par trunguy

Merci pour les explications.
Pour ceux qui ont un peu de bagou, je pense que ça reste relativement intuitif. Pour les débutants Java, je ne peux que leur souhaiter bonne chance pour bien les tenants et aboutissants de cette usine (pas à gaz) ^^.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.