ZonedDateTime
java.time.ZonedDateTime is a date and time in a specific time zone, such as 2026-11-20T19:30+05:30[Asia/Kolkata]. It combines a LocalDateTime with a ZoneId and the zone's current offset from UTC, so it identifies one exact moment while still knowing the local wall-clock time. It is the right type for anything scheduled for a place (a meeting in Chennai, a flight departing London, a sale ending at midnight in New York), and the only java.time type whose arithmetic follows daylight-saving rules.
Time Zones and Offsets
Two related ideas are easy to confuse:
ZoneOffset | ZoneId | |
|---|---|---|
| Example | +05:30, -05:00, Z | Asia/Kolkata, America/New_York |
| Meaning | A fixed difference from UTC | A region with rules for its offset over time |
| Changes | Never | Can differ by season (DST) and by historical or future law |
America/New_York is -05:00 in winter and -04:00 in summer. Europe/London is Z (UTC+0) in winter and +01:00 in summer. Asia/Kolkata is +05:30 all year. A ZoneId contains these rules, using the IANA time zone database shipped with the JDK (about 600 zone IDs).
Use region IDs like Asia/Kolkata, not abbreviations such as IST, which are ambiguous (IST also means Irish and Israel Standard Time).
Creating a ZonedDateTime
import java.time.*;
ZoneId kolkata = ZoneId.of("Asia/Kolkata");
ZonedDateTime now = ZonedDateTime.now(kolkata);
ZonedDateTime webinar = ZonedDateTime.of(2026, 11, 20, 19, 30, 0, 0, kolkata);
ZonedDateTime fromLocal = LocalDateTime.of(2026, 11, 20, 19, 30).atZone(kolkata);
ZonedDateTime parsed = ZonedDateTime.parse("2026-11-20T19:30+05:30[Asia/Kolkata]");
ZonedDateTime fromInstant = Instant.parse("2026-11-20T14:00:00Z").atZone(kolkata);
webinar.getOffset(); // +05:30
webinar.getZone(); // Asia/Kolkata
webinar.toInstant(); // 2026-11-20T14:00:00Z
webinar.toLocalDateTime(); // 2026-11-20T19:30
webinar.toOffsetDateTime(); // 2026-11-20T19:30+05:30
Converting Between Zones
Two methods change the zone, with very different meanings:
ZonedDateTime ist = ZonedDateTime.of(2026, 11, 20, 19, 30, 0, 0, ZoneId.of("Asia/Kolkata"));
ist.withZoneSameInstant(ZoneId.of("Europe/London")); // 2026-11-20T14:00Z[Europe/London]
ist.withZoneSameLocal(ZoneId.of("Europe/London")); // 2026-11-20T19:30Z[Europe/London]
| Method | Keeps | Changes | Use for |
|---|---|---|---|
withZoneSameInstant | The moment | The wall-clock time | "What time is this meeting for my colleague in London?" |
withZoneSameLocal | The wall-clock time | The moment | "Same local time, but in another city" (rarely what you want) |
Displaying a single event to users in different countries always uses withZoneSameInstant (or instant.atZone(zone)).
Daylight-Saving Time
DST makes local time discontinuous twice a year. ZonedDateTime handles both cases using the zone's rules.
The spring gap
In New York, at 2:00 am on 8 March 2026, clocks jump to 3:00 am. Times from 2:00 to 2:59 do not exist that day:
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime.of(LocalDateTime.of(2026, 3, 8, 2, 30), ny);
// 2026-03-08T03:30-04:00[America/New_York]: shifted forward by the length of the gap
Measuring across the gap gives the true elapsed time:
ZonedDateTime before = ZonedDateTime.of(LocalDateTime.of(2026, 3, 8, 1, 0), ny); // -05:00
ZonedDateTime after = ZonedDateTime.of(LocalDateTime.of(2026, 3, 8, 4, 0), ny); // -04:00
Duration.between(before, after); // PT2H: only two real hours passed
The same calculation with LocalDateTime returns three hours.
The autumn overlap
On 1 November 2026, New York clocks go back from 2:00 to 1:00, so 1:30 occurs twice. ZonedDateTime chooses the earlier offset by default, and you can request the later one:
ZonedDateTime first = ZonedDateTime.of(LocalDateTime.of(2026, 11, 1, 1, 30), ny);
// 2026-11-01T01:30-04:00[America/New_York]
first.withLaterOffsetAtOverlap();
// 2026-11-01T01:30-05:00[America/New_York]: one hour later in real time
Adding days versus adding hours
before.plusDays(1); // 2026-03-09T01:00-04:00: same wall-clock time tomorrow
before.plusHours(24); // 2026-03-09T02:00-04:00: exactly 24 hours later
Across a DST change, "one day later" and "24 hours later" are different. plusDays keeps the local time (right for "the same meeting tomorrow"); plusHours(24) keeps the exact duration (right for "this token expires in 24 hours").
India (Asia/Kolkata) does not observe DST, but any system with users or servers in other countries must handle these rules, which is exactly what ZonedDateTime does.
ZonedDateTime Versus OffsetDateTime and Instant
| Type | Contains | Best for |
|---|---|---|
Instant | A moment in UTC | Storing and comparing when things happened |
OffsetDateTime | Local date-time + fixed offset | Exchanging timestamps in APIs, databases (TIMESTAMP WITH TIME ZONE) |
ZonedDateTime | Local date-time + offset + zone rules | Scheduling, user display, calendar arithmetic across DST |
A typical architecture stores Instant or OffsetDateTime values in UTC, and converts to ZonedDateTime in the user's zone for display and for date arithmetic that must respect local rules.
Comparing ZonedDateTimes
isBefore, isAfter, and isEqual compare the instant, so values in different zones compare correctly:
ZonedDateTime a = ZonedDateTime.parse("2026-11-20T19:30+05:30[Asia/Kolkata]");
ZonedDateTime b = ZonedDateTime.parse("2026-11-20T14:00Z[Europe/London]");
a.isEqual(b); // true: the same moment
a.equals(b); // false: equals also compares the local date-time and zone
Use isEqual (or compare toInstant() values) to ask "is this the same moment?".
Practical Example: Finding a Meeting Time Across Zones
A team is spread across Chennai and London, and possibly New York. A meeting slot must fall within 9:00–18:00 local time on a weekday for every member.
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class Meeting {
static final LocalTime WORK_START = LocalTime.of(9, 0);
static final LocalTime WORK_END = LocalTime.of(18, 0);
static boolean withinWorkHours(ZonedDateTime start, Duration length, ZoneId zone) {
ZonedDateTime s = start.withZoneSameInstant(zone);
ZonedDateTime e = s.plus(length);
DayOfWeek d = s.getDayOfWeek();
return d != DayOfWeek.SATURDAY && d != DayOfWeek.SUNDAY
&& !s.toLocalTime().isBefore(WORK_START)
&& !e.toLocalTime().isAfter(WORK_END)
&& s.toLocalDate().equals(e.toLocalDate());
}
static List<ZonedDateTime> commonSlots(LocalDate date, List<ZoneId> team, Duration length) {
List<ZonedDateTime> options = new ArrayList<>();
ZonedDateTime t = date.atStartOfDay(ZoneOffset.UTC);
ZonedDateTime end = t.plusDays(1);
for (; t.isBefore(end); t = t.plusMinutes(30)) {
final ZonedDateTime slot = t;
if (team.stream().allMatch(z -> withinWorkHours(slot, length, z))) options.add(slot);
}
return options;
}
public static void main(String[] args) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("HH:mm");
LocalDate date = LocalDate.of(2026, 11, 17);
List<ZoneId> team = List.of(ZoneId.of("Asia/Kolkata"), ZoneId.of("Europe/London"));
List<ZonedDateTime> slots = commonSlots(date, team, Duration.ofMinutes(30));
System.out.println(slots.size() + " slots for Chennai + London on " + date + ":");
for (ZonedDateTime s : List.of(slots.get(0), slots.get(slots.size() - 1))) {
System.out.println(" Chennai " + s.withZoneSameInstant(team.get(0)).format(fmt)
+ " London " + s.withZoneSameInstant(team.get(1)).format(fmt) + " (UTC " + s.format(fmt) + ")");
}
List<ZoneId> withNy = new ArrayList<>(team);
withNy.add(ZoneId.of("America/New_York"));
System.out.println("Adding New York: " + commonSlots(date, withNy, Duration.ofMinutes(30)).size() + " slots");
}
}
Output:
7 slots for Chennai + London on 2026-11-17:
Chennai 14:30 London 09:00 (UTC 09:00)
Chennai 17:30 London 12:00 (UTC 12:00)
Adding New York: 0 slots
The program steps through the day in UTC, half an hour at a time, and converts each candidate moment into every member's zone with withZoneSameInstant. A slot qualifies only if, in each zone, it falls on a weekday, starts at or after 9:00, ends by 18:00, and does not run past midnight. In November, London is five and a half hours behind Chennai, so working hours overlap from 09:00 to 12:30 UTC, giving seven half-hour slots, the first and last of which are shown. New York's working day (14:00–23:00 UTC in November) does not overlap with Chennai's (03:30–12:30 UTC) at all, so adding it leaves no common slot. Because offsets come from each zone's rules, the same code gives correct results in summer, when London and New York shift their clocks and the overlaps change.