Demo: a meeting across time zones and a daylight-saving change
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 anInstant, and explain why theInstantis what you store. - Show one meeting in several zones with
withZoneSameInstantoratZoneand a formatter. - Read the offsets in the output and explain why the time difference between cities changes during the year.
- Recognize and fix the
LocalDateTimemistake. - 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.
- Create the meeting as 09:00 in
Europe/Londonon a chosen date. - Convert it to an
Instant(this is what would go into storage) and print it. - Print it in London, Riyadh and New York, with each offset.
- Repeat for several dates around the daylight saving changes.
- Compute the days until a deadline.
- Make the mistake on purpose and fix it.
Step 1: the program
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 ofof: it takes aLocalDate, aLocalTimeand a zone. It builds the meeting as "09:00 on that date, in London"..toInstant()removes the zone and keeps the moment. The methodshowreceives only anInstant: 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
xxxprints the offset as+01:00style text, andEEEthe abbreviated day name. Both are pattern letters in theDateTimeFormatterJavadoc; the text letters depend on the locale, so the formatter hasLocale.ENGLISH. - The formatter is a
private static finalconstant, as Lesson 4 recommended.
Step 2: the output
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:
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));45 days, P1M14DBecause 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:
LocalDateTime m = LocalDateTime.of(2026, 3, 30, 9, 0);
System.out.println(m.withZoneSameInstant(ZoneId.of("Asia/Riyadh")));The compiler refuses:
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 LocalDateTimeThere 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:
LocalDateTime naive = LocalDateTime.of(2026, 3, 30, 9, 0);
System.out.println(naive.atZone(london).withZoneSameInstant(ZoneId.of("America/New_York")));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)
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)
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)
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));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
Instantonce, 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:
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.
long days = ChronoUnit.DAYS.between(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 5, 31)); // 91From 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.
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:
- Take a meeting as a
LocalDate, aLocalTimeand a zone ID (asString). - Build the
ZonedDateTime, convert to anInstant, and put a comment above that line saying why theInstantis what you store. - Print the meeting in three zones with one formatter constant.
- Take a deadline
LocalDateand print the days until it from a date you pass in. - Run it for dates on both sides of one daylight saving change and read the offsets.
Check your work:
- The meeting shows the same
Instantin every zone's line. - The offsets differ by date as expected.
- There is no
LocalDateTimeused for a conversion, and no hard-coded offset.
Summary & Next Lesson
Key takeaways
- Schedule in a named zone with
ZonedDateTime; store theInstant; convert to other zones withatZoneorwithZoneSameInstantwhen displaying. - The time difference between two cities changes during the year because they change their clocks on different dates.
LocalDateTimecannot be converted to another zone; say which zone it was written in withatZone, or useZonedDateTimefrom the start.- Use
plusDaysfor wall-clock recurrence andDurationfor exact elapsed time. ChronoUnit.DAYS.betweenonLocalDatevalues 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
- Which type do you store for "when the meeting happens"?
- Why did the stored instant change between 16 March and 30 March for a "09:00 London" meeting?
- What does the compiler say when you call
withZoneSameInstanton aLocalDateTime? - When would you use
plusHours(24)instead ofplusDays(1)? - Why avoid
ZoneId.systemDefault()in business logic?
Show answers
Instant(UTC).- London changed to summer time on 29 March, so 09:00 London moved from 09:00 UTC to 08:00 UTC.
cannot find symbol, because the method does not exist onLocalDateTime, which has no zone.- When you mean exactly 24 elapsed hours, such as an expiry time, not "tomorrow at the same clock time".
- 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
Instantas the UTC timeline point andatZone. - 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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.