المدد والفترات والتنسيق
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
You can now name dates, times and moments. Two jobs remain. First, measuring: how many hours is a shift, how many days until a deadline, how long since a user signed up. Second, text: showing a date as 15 Mar 2026 on a screen, or reading 15/03/2026 typed by a user.
Java answers the first with two types that look alike and mean different things: Duration for time-based amounts (hours, minutes, seconds) and Period for date-based amounts (years, months, days). It answers the second with DateTimeFormatter. We keep the world-clock picture: a duration is how far the clock hands travelled, a period is how many pages the calendar turned.
You need Lessons 1 to 3 and exceptions from BEG-029.
Learning Objectives
By the end of this lesson you will be able to:
- Choose between
DurationandPeriodfor a requirement. - Compute the amount between two values with
Duration.between,Period.betweenandChronoUnit.X.between. - Read parts of a result with
toHours(),toMinutesPart(),getMonths()andgetDays(). - Format a date with
DateTimeFormatter.ofPatternand parse text back into a date. - Explain why pattern letters are case-sensitive (
MMversusmm). - Explain why
plusDays(1)andplusHours(24)can differ across a daylight saving change.
Written Explanation
Duration: an exact amount of time
A Duration measures time in seconds and nanoseconds. The Javadoc calls it a fixed length of time. Use it for hours, minutes and seconds.
Duration d = Duration.between(LocalTime.of(8, 30), LocalTime.of(17, 45));
System.out.println(d); // PT9H15M
System.out.println(d.toHours()); // 9 (whole hours)
System.out.println(d.toMinutes()); // 555 (the whole duration in minutes)
System.out.println(d.toMinutesPart()); // 15 (the minutes left after the hours)PT9H15M is the standard text form: P for period, T before the time part, 9 hours and 15 minutes. Two similar-looking families of methods matter:
toHours(),toMinutes(),toSeconds()convert the whole duration to that unit.toHoursPart(),toMinutesPart(),toSecondsPart()give the part of that unit within the next larger one, which is what you display as9:15.
Create durations with Duration.ofMinutes(135), Duration.ofHours(2), and so on. The result of between is negative when the end is before the start; call abs() for the size. Duration.between needs types that hold a time: it works for LocalTime, LocalDateTime, Instant and ZonedDateTime, but not for LocalDate, which has no seconds:
Unsupported unit: SecondsA Duration treats a day as exactly 24 hours. Duration.ofDays(2) prints as PT48H, never as days.
Period: a calendar amount
A Period is years, months and days. Its real length depends on the dates it is applied to, because months differ.
Period p = Period.between(LocalDate.of(2025, 11, 20), LocalDate.of(2026, 3, 15));
System.out.println(p + " " + p.getMonths() + " " + p.getDays()); // P3M23D 3 23P3M23D is three months and 23 days. Notice that Period keeps the parts separate: it does not convert months into days. Period.of(0, 1, 40) stays P1M40D. If you need the total number of days, use ChronoUnit.
ChronoUnit: one number in one unit
ChronoUnit.DAYS.between(a, b) counts the complete days between two values and returns a long:
long days = ChronoUnit.DAYS.between(LocalDate.of(2026, 3, 15), LocalDate.of(2026, 12, 31));
// 291Other units exist: HOURS, MONTHS, YEARS, and so on. Pick ChronoUnit when you want a single total; pick Period when you want "3 months and 23 days"; pick Duration for exact elapsed time.
| You want | Use |
|---|---|
| "9 hours 15 minutes between two times" | Duration.between |
| "3 months and 23 days between two dates" | Period.between |
| "291 days until 31 December" | ChronoUnit.DAYS.between |
Days versus 24 hours across daylight saving
ZonedDateTime offers both kinds of addition. Date units (plusDays) work on the local calendar; time units (plusHours) work on the exact timeline. The Javadoc says so, and it matters on the spring change day in New York (8 March 2026):
ZonedDateTime s = ZonedDateTime.of(2026, 3, 7, 12, 0, 0, 0, ZoneId.of("America/New_York"));
s.plusDays(1); // 2026-03-08T12:00-04:00[America/New_York] same wall clock, a 23-hour gap
s.plusHours(24); // 2026-03-08T13:00-04:00[America/New_York] 24 real hours laterSo plusDays(1) means "tomorrow, same clock time" and plusHours(24) means "exactly 24 hours later". Likewise Period.ofDays(1) follows the calendar and Duration.ofDays(1) is 24 hours. Choose by meaning: a daily 09:00 meeting uses days; a 24-hour cache expiry uses hours.
Formatting and parsing with DateTimeFormatter
A DateTimeFormatter turns a date-time into text (format) and text into a date-time (parse) using a pattern.
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH);
LocalDate date = LocalDate.of(2026, 3, 15);
System.out.println(f.format(date)); // 15 Mar 2026
System.out.println(date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))); // 15/03/2026
LocalDate back = LocalDate.parse("15/03/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy"));Common pattern letters, from the Javadoc:
| Letter | Meaning | Example |
|---|---|---|
yyyy | year | 2026 |
MM | month as a number | 03 |
MMM | month as short text | Mar |
MMMM | month as full text | March |
dd | day of month | 15 |
HH | hour of day, 0 to 23 | 21 |
hh / h with a | hour 1 to 12, with an AM/PM marker | 9 PM |
mm | minute | 05 |
ss | second | 09 |
Case matters. MM is month and mm is minute. Writing yyyy-mm-dd compiles and runs, and silently prints the minute where the month should be (for 15 March 2026 at 09:05 it printed 2026-05-15). The same goes for HH:MM, which put the month 03 where the minutes belong.
Year letter. For ordinary dates yyyy works as expected; the Javadoc describes y as year-of-era and u as the proleptic year. For modern dates they give the same text, but uuuu is safer when you use strict parsing (see below).
Text depends on locale. MMM and a produce words, and the words depend on the Locale. ofPattern(pattern) uses the machine's default; ofPattern(pattern, Locale.ENGLISH) gives the same output everywhere. Pass a locale whenever the pattern has words.
Formatters are immutable and thread-safe, so keep them in a private static final constant instead of building one for each call.
Not every field exists in every type. Formatting a LocalDate with a time pattern throws:
Unsupported field: HourOfDayParsing: errors and leniency
LocalDate.parse(text, formatter) throws DateTimeParseException when the text does not match, with a message such as Text '2026-03-15' could not be parsed at index 2.
A surprise: with a default formatter built with yyyy, the text 31/02/2026 does not fail. It silently becomes 2026-02-28. The default resolver style is SMART, which adjusts a day of month up to 31 to the last valid day. If you want invalid dates rejected, use uuuu and .withResolverStyle(ResolverStyle.STRICT); then the message is Text '31/02/2026' could not be parsed: Invalid date 'FEBRUARY 31'. A day of 32 fails in either mode.
DateTimeFormatter strict = DateTimeFormatter.ofPattern("dd/MM/uuuu")
.withResolverStyle(ResolverStyle.STRICT);Examples
Example 1: working hours and the days left (simple)
Duration d = Duration.between(LocalTime.of(8, 30), LocalTime.of(17, 45));
System.out.println(d + " " + d.toHours() + " " + d.toMinutes() + " " + d.toMinutesPart());
long days = ChronoUnit.DAYS.between(LocalDate.of(2026, 3, 15), LocalDate.of(2026, 12, 31));
System.out.println(days);PT9H15M 9 555 15
291The shift is 9 hours 15 minutes, which is 555 minutes in total, and from 15 March to 31 December 2026 there are 291 days. The date is fixed in the example on purpose, so the output never depends on today's date.
Example 2: three ways to measure one gap (practical)
LocalDate from = LocalDate.of(2025, 11, 20);
LocalDate to = LocalDate.of(2026, 3, 15);
System.out.println(Period.between(from, to));
System.out.println(ChronoUnit.MONTHS.between(from, to));
System.out.println(ChronoUnit.DAYS.between(from, to));P3M23D
3
115The same gap is "3 months and 23 days", "3 whole months" and "115 days". None is wrong; they answer different questions. A subscription screen may show the first, a billing rule the second, and a countdown the third.
Example 3: reading user input safely (realistic)
A form accepts a date as dd/MM/yyyy and prints it back in a friendlier way.
DateTimeFormatter in = DateTimeFormatter.ofPattern("dd/MM/uuuu").withResolverStyle(ResolverStyle.STRICT);
DateTimeFormatter out = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH);
for (String text : new String[]{"15/03/2026", "31/02/2026", "2026-03-15"}) {
try {
System.out.println(LocalDate.parse(text, in).format(out));
} catch (DateTimeParseException e) {
System.out.println("rejected: " + e.getMessage());
}
}15 Mar 2026
rejected: Text '31/02/2026' could not be parsed: Invalid date 'FEBRUARY 31'
rejected: Text '2026-03-15' could not be parsed at index 2Separating the input format from the output format, plus strict parsing, makes the program reject impossible dates instead of silently fixing them. The exact message text comes from JDK 21 and may differ in other versions, so do not compare messages in code.
Backend Insight
Servers exchange dates as text: JSON bodies, CSV files (BEG-034 teaches files), log lines. Good practice:
- Use the ISO form (
2026-03-15,2026-05-04T07:00:00Z) in APIs and storage. It sorts correctly as text and every platform reads it.Instant.parseandLocalDate.parsehandle it with no formatter. - Use custom patterns only at the edges where humans read or type dates, and write that pattern once, as a constant.
- Do arithmetic on the typed values, never on strings.
- Say in code or documentation whether an interval is exact (
Duration) or calendar-based (Period). A "trial of 30 days" and a "trial of 720 hours" are different promises across a daylight saving change.
Common Mistakes
Mistake 1: mm for month and MM for minute
MM is month, mm is minute. The code compiles and prints nonsense.
Mistake 2: Duration.between on LocalDate
Dates have no time part, so this throws DateTimeException (Unsupported unit: Seconds). Use ChronoUnit.DAYS.between or Period.between.
Mistake 3: reading toHours() as the "hours part"
toHours() is the whole duration in hours, and toMinutes() the whole duration in minutes. Use toMinutesPart() for the minutes left over.
Mistake 4: forgetting the locale
MMM prints Mar in English and other words in other locales. Pass Locale.ENGLISH (or the right locale) when the pattern has text.
Mistake 5: trusting lenient parsing
31/02/2026 became 2026-02-28 under the default resolver. Use strict mode when invalid input should be rejected.
Exercises
Try each exercise yourself before opening the solutions. Running code depends on your IDE or terminal.
Level A: Foundation
A1. Choose Duration or Period: (a) the length of a meeting, (b) the time until someone's 18th birthday expressed in years, months and days, (c) how long a cache entry lives.
أظهر الحل A1
A1. (a) Duration. (b) Period. (c) Duration.
A2. Write the call that prints LocalDate.of(2026, 3, 15) as 15/03/2026.
أظهر الحل A2
A2. LocalDate.of(2026, 3, 15).format(DateTimeFormatter.ofPattern("dd/MM/yyyy")).
A3. In one sentence each: what do MM and mm mean?
أظهر الحل A3
A3. MM is the month as a number (03). mm is the minute of the hour (05).
Level B: Application
B1. For 10 January 2026 to 25 April 2026, what are Period.between and ChronoUnit.DAYS.between?
أظهر الحل B1
B1. Period.between(LocalDate.of(2026, 1, 10), LocalDate.of(2026, 4, 25)) is P3M15D, and ChronoUnit.DAYS.between(...) with the same arguments is 105. Both were confirmed by running the code.
B2. Parse the text 7 Sep 2026 into a LocalDate. Which pattern and which locale do you use?
أظهر الحل B2
B2.
LocalDate d = LocalDate.parse("7 Sep 2026", DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH));
// 2026-09-07Use a single d because the day has no leading zero, and give the English locale because Sep is English text.
B3. A night shift starts at 22:00 on 15 March 2026 and ends at 06:00 on 16 March. Compute its duration. Explain why Duration.between(LocalTime.of(22, 0), LocalTime.of(6, 0)) is not what you want.
أظهر الحل B3
B3. Duration.between(LocalDateTime.of(2026, 3, 15, 22, 0), LocalDateTime.of(2026, 3, 16, 6, 0)) is PT8H. With bare LocalTime values there is no date, so the second time is simply earlier than the first, and the result is the negative PT-16H.
Level C: Challenge
C1. Explain the difference between s.plusDays(1) and s.plusHours(24) for a ZonedDateTime in New York at noon on 7 March 2026, and give both results.
أظهر الحل C1
C1. plusDays(1) gives 2026-03-08T12:00-04:00[America/New_York] (same wall clock, but only 23 hours later because the clocks jumped). plusHours(24) gives 2026-03-08T13:00-04:00[America/New_York] (exactly 24 hours later). Use days for "tomorrow at the same time" and hours for an exact elapsed time.
C2. A user types 31/02/2026 and your lenient formatter returns 28 February. Write a formatter that rejects it, and explain what changed.
أظهر الحل C2
C2.
DateTimeFormatter strict = DateTimeFormatter.ofPattern("dd/MM/uuuu")
.withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("31/02/2026", strict); // DateTimeParseExceptionThe year letter becomes uuuu (strict mode needs it or an era) and the resolver style is STRICT, so an impossible day is rejected instead of adjusted to the last day of the month.
Mini Challenge
Write ShiftReport. Given a list of shifts as pairs of LocalDateTime (start and end), print each shift as 15 Mar 2026 22:00 -> 16 Mar 2026 06:00 (8h 0m) using one formatter constant and Duration parts, and then the total hours of all shifts. Include a shift that crosses midnight.
Check your work:
- You used
HHfor the hour andmmfor minutes, and passed a locale. - The crossing-midnight shift shows the right length.
- The total uses
Duration.plusor a sum of minutes, not a sum of strings.
Bonus: add a method that parses dd/MM/yyyy HH:mm input strictly.
Summary & Next Lesson
Key takeaways
Durationis an exact amount of time (hours, minutes, seconds);Periodis a calendar amount (years, months, days).ChronoUnit.X.between(a, b)returns one number in one unit.toHours()is the whole duration in hours;toMinutesPart()is the leftover minutes.DateTimeFormatter.ofPatternformats and parses.MMis month,mmis minute; pass a locale for text.plusDaysfollows the calendar andplusHoursfollows the timeline; they can differ at a daylight saving change.- The default parser is lenient about the last days of a month; use strict mode to reject impossible dates.
Next lesson
The next lesson, "Demo: a meeting across time zones and a daylight-saving change", puts everything together: schedule a meeting, show it in other zones, watch the offsets change, and make the planned mistake with LocalDateTime.
Self-check
- Which type is for hours and minutes,
DurationorPeriod? - What does
Duration.ofDays(2)print? - What is wrong with the pattern
yyyy-mm-dd? - What does
ChronoUnit.DAYS.between(a, b)return? - Why pass
Locale.ENGLISHtoofPattern("dd MMM yyyy", ...)?
أظهر الإجابات
Duration.PT48H; aDurationday is exactly 24 hours and it never prints days.mmmeans minutes, not month; useMM.- The number of whole days between
aandb, as along. MMMis text, and the text depends on the locale; passing the locale makes the output the same on every machine.
References
- Oracle, java.time.Duration (Java SE 21). Supports
Durationas a fixed length of time, a day counted as 24 hours,Duration.betweenrequirements, and negative results. - Oracle, java.time.format.DateTimeFormatter (Java SE 21). Supports the pattern letters, case sensitivity of
Mandm, locale dependence,yversusu, and immutability. - Oracle, java.time.ZonedDateTime (Java SE 21). Supports date units operating on the local time-line and time units on the instant time-line.
- Outputs were produced on JDK 21. The statements about the SMART resolver silently clamping
31/02and about the exact exception messages come from running the code, not from a documentation page I read; the message wording may vary between JDK versions.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.