Skip to content
Ameen Abozaid
PublishedBEG-033-L04

Durations, periods, formatting

  • Lesson 4 of 5
  • 11 min read
  • Beginner
On This Lesson 11 topics

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 Duration and Period for a requirement.
  • Compute the amount between two values with Duration.between, Period.between and ChronoUnit.X.between.
  • Read parts of a result with toHours(), toMinutesPart(), getMonths() and getDays().
  • Format a date with DateTimeFormatter.ofPattern and parse text back into a date.
  • Explain why pattern letters are case-sensitive (MM versus mm).
  • Explain why plusDays(1) and plusHours(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.

java
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 as 9: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:

text
Unsupported unit: Seconds

A 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.

java
Period p = Period.between(LocalDate.of(2025, 11, 20), LocalDate.of(2026, 3, 15));
System.out.println(p + " " + p.getMonths() + " " + p.getDays());   // P3M23D 3 23

P3M23D 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:

java
long days = ChronoUnit.DAYS.between(LocalDate.of(2026, 3, 15), LocalDate.of(2026, 12, 31));
// 291

Other 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 wantUse
"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):

java
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 later

So 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.

java
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:

LetterMeaningExample
yyyyyear2026
MMmonth as a number03
MMMmonth as short textMar
MMMMmonth as full textMarch
ddday of month15
HHhour of day, 0 to 2321
hh / h with ahour 1 to 12, with an AM/PM marker9 PM
mmminute05
sssecond09

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:

text
Unsupported field: HourOfDay

Parsing: 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.

java
DateTimeFormatter strict = DateTimeFormatter.ofPattern("dd/MM/uuuu")
        .withResolverStyle(ResolverStyle.STRICT);

Examples

Example 1: working hours and the days left (simple)

java
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);
text
PT9H15M 9 555 15
291

The 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)

java
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));
text
P3M23D
3
115

The 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.

java
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());
    }
}
text
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 2

Separating 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.parse and LocalDate.parse handle 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.

Show solution A1

A1. (a) Duration. (b) Period. (c) Duration.

A2. Write the call that prints LocalDate.of(2026, 3, 15) as 15/03/2026.

Show solution A2

A2. LocalDate.of(2026, 3, 15).format(DateTimeFormatter.ofPattern("dd/MM/yyyy")).

A3. In one sentence each: what do MM and mm mean?

Show solution 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?

Show solution 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?

Show solution B2

B2.

java
LocalDate d = LocalDate.parse("7 Sep 2026", DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH));
// 2026-09-07

Use 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.

Show solution 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.

Show solution 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.

Show solution C2

C2.

java
DateTimeFormatter strict = DateTimeFormatter.ofPattern("dd/MM/uuuu")
        .withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("31/02/2026", strict);   // DateTimeParseException

The 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:

  1. You used HH for the hour and mm for minutes, and passed a locale.
  2. The crossing-midnight shift shows the right length.
  3. The total uses Duration.plus or 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

  • Duration is an exact amount of time (hours, minutes, seconds); Period is 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.ofPattern formats and parses. MM is month, mm is minute; pass a locale for text.
  • plusDays follows the calendar and plusHours follows 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

  1. Which type is for hours and minutes, Duration or Period?
  2. What does Duration.ofDays(2) print?
  3. What is wrong with the pattern yyyy-mm-dd?
  4. What does ChronoUnit.DAYS.between(a, b) return?
  5. Why pass Locale.ENGLISH to ofPattern("dd MMM yyyy", ...)?
Show answers
  1. Duration.
  2. PT48H; a Duration day is exactly 24 hours and it never prints days.
  3. mm means minutes, not month; use MM.
  4. The number of whole days between a and b, as a long.
  5. MMM is 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 Duration as a fixed length of time, a day counted as 24 hours, Duration.between requirements, and negative results.
  • Oracle, java.time.format.DateTimeFormatter (Java SE 21). Supports the pattern letters, case sensitivity of M and m, locale dependence, y versus u, 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/02 and 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.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.