Skip to content
Ameen Abozaid
PublishedBEG-033-L05

Demo: a meeting across time zones and a daylight-saving change

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

Lesson Overview

This is a worked demo. You will build a small meeting planner step by step, using only what Lessons 1 to 4 taught: ZonedDateTime, Instant, withZoneSameInstant, DateTimeFormatter, ChronoUnit and Period. A meeting is scheduled at 09:00 London time; the program stores it as an Instant and shows it in Riyadh and New York. Then we place the same meeting on dates around the March daylight saving changes and watch the offsets move. Finally we make the planned mistake, a LocalDateTime where a zone was needed, and see how the compiler stops us.

This demo prepares the lab for the module: schedule a meeting in one time zone, show it in three, and count days to a deadline, correct across a daylight saving change. Read the demo, then type it yourself rather than pasting it, and run it on your own machine (how you run it depends on your IDE or terminal).

Learning Objectives

By the end of this lesson you will be able to:

  • Schedule a meeting as a ZonedDateTime, convert it to an Instant, and explain why the Instant is what you store.
  • Show one meeting in several zones with withZoneSameInstant or atZone and a formatter.
  • Read the offsets in the output and explain why the time difference between cities changes during the year.
  • Recognize and fix the LocalDateTime mistake.
  • Compute days until a deadline with ChronoUnit.DAYS.
  • Design tests whose results do not depend on today's date.

Written Explanation

The plan

The steps follow the module's idea: think in moments, store UTC, convert at the edge.

  1. Create the meeting as 09:00 in Europe/London on a chosen date.
  2. Convert it to an Instant (this is what would go into storage) and print it.
  3. Print it in London, Riyadh and New York, with each offset.
  4. Repeat for several dates around the daylight saving changes.
  5. Compute the days until a deadline.
  6. Make the mistake on purpose and fix it.

Step 1: the program

java
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Locale;

public class MeetingPlanner {
    private static final DateTimeFormatter OUT =
            DateTimeFormatter.ofPattern("EEE dd MMM yyyy HH:mm (xxx)", Locale.ENGLISH);

    static void show(Instant meeting, List<String> zones) {
        System.out.println("Stored: " + meeting);
        for (String id : zones) {
            System.out.println(String.format("%-17s", id) + meeting.atZone(ZoneId.of(id)).format(OUT));
        }
    }

    public static void main(String[] args) {
        ZoneId london = ZoneId.of("Europe/London");
        List<String> zones = List.of("Europe/London", "Asia/Riyadh", "America/New_York");
        for (LocalDate day : List.of(LocalDate.of(2026, 3, 2), LocalDate.of(2026, 3, 16),
                                     LocalDate.of(2026, 3, 30))) {
            Instant meeting = ZonedDateTime.of(day, LocalTime.of(9, 0), london).toInstant();
            show(meeting, zones);
            System.out.println();
        }
    }
}

Points to notice:

  • ZonedDateTime.of(date, time, zone) is another form of of: it takes a LocalDate, a LocalTime and a zone. It builds the meeting as "09:00 on that date, in London".
  • .toInstant() removes the zone and keeps the moment. The method show receives only an Instant: it does not know or care where the meeting was planned.
  • String.format("%-17s", id) pads the zone name so the columns line up. It is not part of the date API.
  • The formatter pattern xxx prints the offset as +01:00 style text, and EEE the abbreviated day name. Both are pattern letters in the DateTimeFormatter Javadoc; the text letters depend on the locale, so the formatter has Locale.ENGLISH.
  • The formatter is a private static final constant, as Lesson 4 recommended.

Step 2: the output

text
Stored: 2026-03-02T09:00:00Z
Europe/London    Mon 02 Mar 2026 09:00 (+00:00)
Asia/Riyadh      Mon 02 Mar 2026 12:00 (+03:00)
America/New_York Mon 02 Mar 2026 04:00 (-05:00)

Stored: 2026-03-16T09:00:00Z
Europe/London    Mon 16 Mar 2026 09:00 (+00:00)
Asia/Riyadh      Mon 16 Mar 2026 12:00 (+03:00)
America/New_York Mon 16 Mar 2026 05:00 (-04:00)

Stored: 2026-03-30T08:00:00Z
Europe/London    Mon 30 Mar 2026 09:00 (+01:00)
Asia/Riyadh      Mon 30 Mar 2026 11:00 (+03:00)
America/New_York Mon 30 Mar 2026 04:00 (-04:00)

Read it like a detective. The meeting is "09:00 in London" on all three days, yet:

  • 2 March. Nobody has changed clocks. London is UTC+0, Riyadh +3, New York -5. The meeting is at 09:00 UTC, 12:00 in Riyadh and 04:00 in New York.
  • 16 March. New York changed its clocks forward on 8 March (the day that has only 23 hours, as Lesson 1 showed), so its offset is now -04:00 and the meeting is at 05:00 there. London has not changed yet. The stored instant is the same time of day, 09:00 UTC.
  • 30 March. London moved forward on 29 March. Now "09:00 in London" is 08:00 UTC, so the stored instant changed. Riyadh, which does not use daylight saving, now sees 11:00. New York still sees 04:00, but only because London's offset moved by the same amount.

The lesson of the table: the gap between two cities is not a constant. London to New York is 5 hours on 2 March, 4 hours on 16 March, and 5 hours again on 30 March. London to Riyadh is 3 hours until London changes, then 2 hours. A program that stores "meeting at 09:00 UTC" and a program that stores "meeting at 09:00 London" disagree after 29 March: decide which one your requirement means. A weekly meeting that must stay at 09:00 London time needs the zone (for example Europe/London) stored beside the rule; a one-off moment needs only the Instant.

The dates 8 and 29 March come from JDK 21's time zone data as printed by this program. Rules differ by country and year, and governments can change them, so check the relevant dates for the year you use rather than trusting these.

Step 3: the deadline

Add this to main:

java
LocalDate today = LocalDate.of(2026, 3, 1);
LocalDate deadline = LocalDate.of(2026, 4, 15);
System.out.println(ChronoUnit.DAYS.between(today, deadline) + " days, "
        + Period.between(today, deadline));
text
45 days, P1M14D

Because dates are calendar facts, LocalDate and ChronoUnit.DAYS are the right pair: the deadline is "15 April" wherever the reader lives. In a real application you would replace the fixed today with LocalDate.now() at the edge of your program, and pass it in, so that the calculation stays testable.

Step 4: the planned mistake

Suppose we had represented the meeting as a LocalDateTime, a date and time with no zone:

java
LocalDateTime m = LocalDateTime.of(2026, 3, 30, 9, 0);
System.out.println(m.withZoneSameInstant(ZoneId.of("Asia/Riyadh")));

The compiler refuses:

text
Bad.java:4: error: cannot find symbol
 System.out.println(m.withZoneSameInstant(ZoneId.of("Asia/Riyadh")));
                     ^
  symbol:   method withZoneSameInstant(ZoneId)
  location: variable m of type LocalDateTime

There is no withZoneSameInstant on LocalDateTime, because without a zone there is no moment to preserve. This is the compiler doing you a favor: the type system tells you the data is too weak. The fix is to say which zone the meeting was written in:

java
LocalDateTime naive = LocalDateTime.of(2026, 3, 30, 9, 0);
System.out.println(naive.atZone(london).withZoneSameInstant(ZoneId.of("America/New_York")));
text
2026-03-30T04:00-04:00[America/New_York]

atZone(london) is the decision "this clock reading was written in London". After it the value is a ZonedDateTime and conversion works.

Be careful: a mistake that compiles is worse. Imagine m.atZone(ZoneId.systemDefault()) instead: it compiles, runs and gives a different answer on a server in a different zone. Always name the zone you mean.

Examples

The three examples below are small variations you can run after the main program.

Example 1: one meeting, three zones (simple)

java
Instant meeting = ZonedDateTime.of(LocalDate.of(2026, 3, 16), LocalTime.of(9, 0),
        ZoneId.of("Europe/London")).toInstant();
System.out.println(meeting.atZone(ZoneId.of("Asia/Riyadh")).toLocalTime());

This prints 12:00. Converting an Instant to a zone and taking toLocalTime() is the shortest way to answer "what is the clock in X".

Example 2: how long until the meeting? (practical)

java
Instant a = Instant.parse("2026-03-16T09:00:00Z");
Instant b = Instant.parse("2026-03-30T08:00:00Z");
System.out.println(Duration.between(a, b).toHours() + " hours");
System.out.println(ChronoUnit.HOURS.between(a, b));

Two meetings that are "both 09:00 London" on 16 and 30 March are 335 hours apart, not 336 (14 days times 24), because London lost an hour in between. Using Duration on instants measures real elapsed time; using plusDays(14) on the local date would keep the clock at 09:00. (The 335 comes from simple arithmetic on the two printed instants: 14 days is 336 hours, minus the one hour between 08:00 and 09:00.) Choose by meaning, as Lesson 4 said.

Example 3: a recurring 09:00 London alarm (realistic)

java
ZonedDateTime s = ZonedDateTime.of(2026, 3, 28, 9, 0, 0, 0, ZoneId.of("Europe/London"));
System.out.println(s.plusDays(1));
System.out.println(s.plusHours(24));
text
2026-03-29T09:00+01:00[Europe/London]
2026-03-29T10:00+01:00[Europe/London]

A daily alarm "at 09:00 London time" must use plusDays(1): it stays at 09:00 on the wall clock even though that day is 23 hours long. If the code used plusHours(24), users would hear the alarm at 10:00 after the change.

Backend Insight

This demo is the pattern of many production features: booking systems, reminders, expiry times, scheduled reports.

  • Accept local input together with an explicit zone (from the user profile or the request).
  • Convert to an Instant once, at the edge, and store that.
  • Convert back to a zone only when producing output. The zone can be the user's or the meeting's own, depending on the requirement.
  • Test around the daylight saving dates of the zones you serve, with fixed dates in the test, not now(). Tests that only run on an ordinary Tuesday miss this whole class of bug.
  • Name the zone every time. ZoneId.systemDefault() makes behavior depend on where the server runs.

Later modules put time columns into databases (JNR-028) and discuss clocks across machines (MID-023). If this demo feels easy, those modules will feel familiar.

Common Mistakes

Mistake 1: LocalDateTime for a meeting in several zones

It cannot be converted. Use ZonedDateTime for the planning zone and Instant for storage.

Mistake 2: testing only on ordinary dates

The bugs sit on the two daylight saving weekends and in the weeks between them. Test those dates.

Mistake 3: plusHours(24) for "tomorrow at the same time"

Use plusDays(1) on a ZonedDateTime when the meaning is a wall-clock time.

Mistake 4: hard-coding offsets

Writing +01:00 for London makes the program wrong for half the year. Use Europe/London.

Mistake 5: assuming the difference between two cities is constant

The demo showed London and New York at 5, 4 and then 5 hours apart within one month.

Exercises

Try each exercise yourself before opening the solutions. Running code depends on your IDE or terminal.

Level A: Foundation

A1. In the demo, why does the show method take an Instant instead of a ZonedDateTime?

Show solution A1

A1. Because everything after the conversion needs only the moment. An Instant has no zone to get wrong, and it is what you would store.

A2. The stored instant on 30 March is 08:00Z, not 09:00Z. Explain why in one sentence.

Show solution A2

A2. London moved to summer time on 29 March, so 09:00 London on 30 March is 08:00 UTC.

A3. Which type and method give you the clock time in Riyadh for an Instant?

Show solution A3

A3. instant.atZone(ZoneId.of("Asia/Riyadh")).toLocalTime().

Level B: Application

B1. Change the program to schedule a 14:00 meeting in Asia/Riyadh on 16 March 2026 and print it in London and New York. What do you expect to see (compute by hand, then run)?

Show solution B1

B1. 14:00 in Riyadh on 16 March is 11:00 UTC. London is still on winter time on 16 March, so it shows 11:00 (+00:00); New York changed on 8 March, so it is at -04:00 and shows 07:00. Code:

java
Instant m = ZonedDateTime.of(LocalDate.of(2026, 3, 16), LocalTime.of(14, 0),
        ZoneId.of("Asia/Riyadh")).toInstant();
show(m, zones);

B2. Write the code that counts the days from 1 March 2026 to 31 May 2026.

Show solution B2

B2.

java
long days = ChronoUnit.DAYS.between(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 5, 31));  // 91

From 1 March to 1 April is 31 days, to 1 May is 30 more, and to 31 May is 30 more: 31 + 30 + 30 = 91. This is hand arithmetic, not a program run.

B3. A teammate writes ZonedDateTime.of(day, LocalTime.of(9, 0), ZoneId.systemDefault()). Name one risk.

Show solution B3

B3. The default zone depends on the machine. A server in another zone produces a different Instant for the same inputs.

Level C: Challenge

C1. Add the autumn dates: schedule 09:00 London on 20 October, 27 October and 3 November 2026, and print the New York clock time and offset. Explain the pattern you see.

Show solution C1

C1. The program (checked by running) printed: 20 October gives instant 08:00Z and New York 04:00 at -04:00; 27 October gives 09:00Z and New York 05:00 at -04:00; 3 November gives 09:00Z and New York 04:00 at -05:00. Pattern: London's offset changes between 20 and 27 October (the instant moves from 08:00Z to 09:00Z), while New York's changes only between 27 October and 3 November (its offset goes from -04:00 to -05:00). In between, the London to New York gap is 4 hours rather than 5. Cities change on different dates.

C2. Write a method Instant nextDaily(ZonedDateTime previous) that returns the next occurrence of a daily meeting at the same wall-clock time in the same zone, and explain why it does not use plusHours.

Show solution C2

C2.

java
static Instant nextDaily(ZonedDateTime previous) {
    return previous.plusDays(1).toInstant();
}

plusDays keeps the wall-clock time in the same zone, so a 09:00 London meeting stays at 09:00 London across the daylight saving change. plusHours(24) adds exactly 24 real hours and drifts by an hour on the change day (Example 3).

Mini Challenge

This is a first draft of the lab, BEG-033-LAB1. Write MeetingPlannerLab:

  1. Take a meeting as a LocalDate, a LocalTime and a zone ID (as String).
  2. Build the ZonedDateTime, convert to an Instant, and put a comment above that line saying why the Instant is what you store.
  3. Print the meeting in three zones with one formatter constant.
  4. Take a deadline LocalDate and print the days until it from a date you pass in.
  5. Run it for dates on both sides of one daylight saving change and read the offsets.

Check your work:

  1. The meeting shows the same Instant in every zone's line.
  2. The offsets differ by date as expected.
  3. There is no LocalDateTime used for a conversion, and no hard-coded offset.

Summary & Next Lesson

Key takeaways

  • Schedule in a named zone with ZonedDateTime; store the Instant; convert to other zones with atZone or withZoneSameInstant when displaying.
  • The time difference between two cities changes during the year because they change their clocks on different dates.
  • LocalDateTime cannot be converted to another zone; say which zone it was written in with atZone, or use ZonedDateTime from the start.
  • Use plusDays for wall-clock recurrence and Duration for exact elapsed time.
  • ChronoUnit.DAYS.between on LocalDate values counts days until a deadline.
  • Test with fixed dates around the daylight saving changes, never only with now().

Next lesson

You have reached the end of this module's lessons. Continue with the module's lab, BEG-033-LAB1 (the meeting planner), and then with the exercises and quiz for BEG-033. The lesson list of the next module is not covered in this module's own specification, so check the course overview for what comes next.

Self-check

  1. Which type do you store for "when the meeting happens"?
  2. Why did the stored instant change between 16 March and 30 March for a "09:00 London" meeting?
  3. What does the compiler say when you call withZoneSameInstant on a LocalDateTime?
  4. When would you use plusHours(24) instead of plusDays(1)?
  5. Why avoid ZoneId.systemDefault() in business logic?
Show answers
  1. Instant (UTC).
  2. London changed to summer time on 29 March, so 09:00 London moved from 09:00 UTC to 08:00 UTC.
  3. cannot find symbol, because the method does not exist on LocalDateTime, which has no zone.
  4. When you mean exactly 24 elapsed hours, such as an expiry time, not "tomorrow at the same clock time".
  5. The result depends on the machine the program runs on, so the same code can give different answers on different servers.

References

  • Oracle, java.time.ZonedDateTime (Java SE 21). Supports withZoneSameInstant, the differing behavior of date units and time units, and gap and overlap handling.
  • Oracle, java.time.format.DateTimeFormatter (Java SE 21). Supports the pattern letters, locale-dependent text and the advice to keep formatters as constants.
  • Oracle, java.time.Instant (Java SE 21). Supports Instant as the UTC timeline point and atZone.
  • The program output, the offsets and the daylight saving dates (8 March and 29 March 2026, and the autumn windows in Exercise C1) come from running the code on JDK 21 with its bundled time zone data. I did not check them against a government or IANA source, and rules for a given year can change.

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.

Type to search levels, phases, modules and lessons.