Java Period
Représentez des durées calendaires (années, mois, jours) en Java avec Period.
Period est le pendant calendaire de Duration. Là où Duration représente « X secondes plus Y nanosecondes », Period représente « X années, Y mois, Z jours ». C'est le bon type pour toute durée exprimée en termes calendaires : « période d'essai de 30 jours », « abonnement annuel », « préavis de deux mois », « ajouter un cycle de facturation à la date de renouvellement ».
Les deux ne se mélangent jamais. Duration.ofDays(30) correspond exactement à 30 × 24 × 3600 secondes. Period.ofDays(30) correspond à 30 jours calendaires, qui n'équivalent pas toujours à 30 × 24 heures (les transitions d'heure d'été ajoutent ou retirent une heure). Pour « exactement ce nombre de secondes », utilisez Duration. Pour « le jour calendaire qui se trouve N jours plus tard », utilisez Period.
Création
Period.ofDays(30);
Period.ofWeeks(2); // stored as 14 days
Period.ofMonths(3);
Period.ofYears(1);
Period.of(1, 6, 0); // 1 year, 6 months, 0 days
Period.between(startDate, endDate); // takes LocalDate (not LocalDateTime)
Period.parse("P1Y2M3D"); // ISO-8601: P[years]Y[months]M[days]DLa forme sous forme de chaîne est PnYnMnD — P1Y2M3D représente un an, deux mois, trois jours. Le préfixe P est obligatoire. Pas de T (cela en ferait une Duration) ; pas d'heures, de minutes ou de secondes (elles n'ont pas leur place ici).
Period.between(start, end) prend deux LocalDates et retourne une décomposition de la différence :
Period age = Period.between(LocalDate.of(1990, 3, 15), LocalDate.of(2025, 11, 4));
// P35Y7M20D — 35 years, 7 months, 20 daysC'est l'idiome standard pour « calculer un âge ». Le résultat est une décomposition, pas un nombre unique — il y a 35 années complètes, puis 7 mois supplémentaires, puis 20 jours. Pour obtenir un seul compteur, utilisez ChronoUnit.YEARS.between(...), qui retourne un long.
Inspection
Period p = Period.of(1, 6, 14);
p.getYears(); // 1
p.getMonths(); // 6
p.getDays(); // 14
p.toTotalMonths(); // 1 * 12 + 6 = 18 (years + months, ignoring days)
p.isZero(); // false
p.isNegative(); // true if any component is negativeTrois accesseurs pour les trois composantes, plus toTotalMonths pour une agrégation rapide. Il n'existe pas de toTotalDays — cela nécessiterait de connaître le contexte calendaire (une année fait 365 ou 366 jours ; un mois fait 28 à 31 jours).
Arithmétique
p.plus(Period.ofMonths(1));
p.plusYears(1);
p.plusMonths(6);
p.plusDays(14);
p.minus(Period.ofDays(7));
p.multipliedBy(3);
p.negated();
p.normalized(); // collapse extra months into yearsnormalized() est intéressant : il regroupe tout nombre de mois égal ou supérieur à 12 en années. Period.of(0, 14, 0).normalized() donne Period.of(1, 2, 0). Il ne touche pas aux jours — il n'y a pas de « normalisation de 31 jours en 1 mois et 1 jour » car les mois n'ont pas une longueur constante.
Ajout à une date
Period est un TemporalAmount. Tout Temporal de type date l'accepte :
LocalDate maturity = LocalDate.of(2025, 11, 4).plus(Period.ofMonths(6));
LocalDate retirement = LocalDate.of(1990, 3, 15).plus(Period.ofYears(65));
LocalDateTime renewal = LocalDateTime.of(2025, 11, 4, 9, 0).plus(Period.ofYears(1));
ZonedDateTime nextBill = zdt.plus(Period.ofMonths(1));Ajouter la partie mois ou année d'un Period à un Instant lève une UnsupportedTemporalTypeException — un Instant est un point sur la ligne de temps sans calendrier, donc le JDK refuse de calculer « un instant un mois plus tard » sans fuseau horaire. (La partie jour est correcte : Instant.plus(Period.ofDays(1)) réussit, car le JDK traite un jour comme exactement 86 400 secondes. Ce sont uniquement les mois et les années qui n'ont pas de longueur fixe et n'ont donc aucun sens sur un Instant.) Lorsque vous avez besoin d'une arithmétique calendaire, convertissez via ZonedDateTime :
Instant nextMonth = inst.atZone(ZoneId.of("UTC"))
.plus(Period.ofMonths(1))
.toInstant();Cette verbosité est délibérée — la conversion est l'endroit où vous fournissez le contexte calendaire manquant.
La règle de clamping de plusMonths de LocalDate s'applique de la même façon à l'arithmétique avec Period : 31 janvier + Period.ofMonths(1) donne le 28 février, pas le 3 mars.
Period ne normalise pas entre les composantes
Un comportement subtil : Period.of(1, 0, 365) n'est pas égal à Period.of(2, 0, 0), même s'ils décrivent la même durée lorsqu'ils sont ajoutés à une date typique. La classe stocke la décomposition telle quelle et compare par structure :
Period.of(1, 0, 365).equals(Period.of(2, 0, 0)); // false
Period.of(0, 14, 0).equals(Period.of(1, 2, 0)); // false (until normalized())
Period.of(0, 14, 0).normalized().equals(Period.of(1, 2, 0)); // truePour « cette période est-elle au moins un an, quelle que soit sa décomposition », comparez sur des dates : start.plus(p1).isEqual(start.plus(p2)) est la seule vérification totalement correcte.
Distance : Period.between vs ChronoUnit.between
Period diff = Period.between(start, end); // calendar breakdown
long days = ChronoUnit.DAYS.between(start, end); // single long
long months = ChronoUnit.MONTHS.between(start, end);
long years = ChronoUnit.YEARS.between(start, end);Les deux répondent à des questions différentes :
Period.between(start, end)retourne « 1 an, 6 mois, 14 jours » — utile quand vous voulez afficher une décomposition.ChronoUnit.DAYS.between(start, end)retourne567(ou quel que soit le nombre littéral de jours) — utile quand vous voulez comparer ou accumuler.
Utilisez le second quand vous avez besoin d'effectuer des calculs sur le résultat. Utilisez le premier quand vous voulez afficher quelque chose à un humain.
Un exemple concret : abonnements, essais et âges
Le programme ci-dessous utilise Period pour un scénario d'abonnement simple : l'essai se termine un mois après l'inscription, la date de renouvellement se répète chaque année, l'âge du client est calculé à partir de sa date de naissance, et le comportement de clamping aux limites des mois est rendu explicite. Il montre également le contraste avec Duration pour « la même durée en temps écoulé ».
Ce que l'on retient de l'exécution :
- Ajouter
Period.ofMonths(1)au 31 janvier a produit le 28 février — la même règle de clamping que dans LocalDate.Period.plusMonths(1).minusMonths(1)n'est pas toujours une identité. Lorsque vous calculez des dates de facturation proches de la fin du mois, gérez explicitement le clamping (par exemple, facturez toujours le 1er du mois suivant) plutôt que de supposer une symétrie aller-retour. Period.between(birth, today)a retourné une décomposition calendaire — années, mois, jours. Pour « est-il majeur ? », utilisezChronoUnit.YEARS.between(birth, today) >= 18, pasage.getYears() >= 18. Les deux donnent la même réponse dans ce cas, mais ils répondent à des questions différentes en général —ChronoUnit.YEARS.betweenreprésente le nombre total d'années entières, tandis queage.getYears()est la composante années de la décomposition.Period.of(0, 14, 0).normalized()est devenuPeriod.of(1, 2, 0). Le nombre de jours n'a pas été touché — les jours ne peuvent pas être normalisés sans savoir quels mois sont concernés. Si vous construisez unPeriodpar arithmétique et voulez une représentation « propre », appeleznormalizedavant de le stocker ou de l'afficher.P1Y.equals(P12M)étaitfalse, etP1Y.equals(P365D)l'était aussi. L'égalité est structurelle, pas par longueur. Appliqué à2024-01-31(une année bissextile),+ P1Yet+ P12Mont tous deux donné2025-01-31, mais+ P365Da donné2025-01-30— un jour de moins, car 2024 compte 366 jours. Ainsi, même « la même longueur » dépend de la date à laquelle on l'applique. Quand vous voulez vraiment savoir « ces deux périodes produisent-elles la même date de fin ? », calculez les deux dates de fin et comparez avecLocalDate.isEqual. La forme.normalized()corrige le cas année/mois mais jamais le cas jour.- L'appel
inst.plus(Period.ofMonths(1))a levé uneUnsupportedTemporalTypeException. UnInstantn'a pas de calendrier, donc un mois (dont la longueur varie) n'a aucun sens sur lui. La partie jour d'unPeriodest correcte sur unInstant— un jour correspond à un nombre fixe de 86 400 secondes — mais les mois et les années ne le sont pas. Convertissez d'abord viaZonedDateTimelorsque vous avez besoin d'une arithmétique calendaire ; le système de types vous oblige à choisir explicitement un fuseau horaire. L'échec symétrique du chapitre Duration (Durationsur uneLocalDate) relève de la même conception : le JDK refuse d'inventer le contexte manquant.
Et ensuite
Period clôt la paire « durées de temps ». Les deux prochains chapitres traitent de la frontière chaîne ↔ valeur : Java Date Formatting pour transformer les valeurs java.time en chaînes, et Java Date Parsing pour l'opération inverse. Tous deux utilisent DateTimeFormatter, le remplaçant moderne et thread-safe du SimpleDateFormat historique.