janv.
2013
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 desInstants
de début et de finDuration
: Durée précise, en millisecondesPeriod
: 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 classejava.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)
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
etMonthDay
(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 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
).
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)
// Â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 tempsMutablePeriod
(modifiable) : variante modifiable dePeriod
.
// Â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 typeProperty
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
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/...
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.
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) ^^.