W3docs

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]D

La forme sous forme de chaîne est PnYnMnDP1Y2M3D 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 days

C'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 negative

Trois 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 years

normalized() 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));  // true

Pour « 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) retourne 567 (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é ».

java— editable, runs on the server

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 ? », utilisez ChronoUnit.YEARS.between(birth, today) >= 18, pas age.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.between représente le nombre total d'années entières, tandis que age.getYears() est la composante années de la décomposition.
  • Period.of(0, 14, 0).normalized() est devenu Period.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 un Period par arithmétique et voulez une représentation « propre », appelez normalized avant de le stocker ou de l'afficher.
  • P1Y.equals(P12M) était false, et P1Y.equals(P365D) l'était aussi. L'égalité est structurelle, pas par longueur. Appliqué à 2024-01-31 (une année bissextile), + P1Y et + P12M ont tous deux donné 2025-01-31, mais + P365D a 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 avec LocalDate.isEqual. La forme .normalized() corrige le cas année/mois mais jamais le cas jour.
  • L'appel inst.plus(Period.ofMonths(1)) a levé une UnsupportedTemporalTypeException. Un Instant n'a pas de calendrier, donc un mois (dont la longueur varie) n'a aucun sens sur lui. La partie jour d'un Period est correcte sur un Instant — un jour correspond à un nombre fixe de 86 400 secondes — mais les mois et les années ne le sont pas. Convertissez d'abord via ZonedDateTime lorsque 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 (Duration sur une LocalDate) 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.

Pratique

Pratique
Un utilisateur s'inscrit le 31 janvier 2025. Votre code de facturation calcule la prochaine charge avec `signupDate.plus(Period.ofMonths(1))`. Quelle est la date de la prochaine charge, et que devez-vous savoir sur ce comportement ?
Un utilisateur s'inscrit le 31 janvier 2025. Votre code de facturation calcule la prochaine charge avec `signupDate.plus(Period.ofMonths(1))`. Quelle est la date de la prochaine charge, et que devez-vous savoir sur ce comportement ?
Was this page helpful?