Learn Java

ZonedDateTime and OffsetDateTime in Java

You probably already know that time in the world is divided into time zones. In programming, there are often situations where you need to work with a specific time zone. The basic time classes (LocalTime and LocalDateTime ) don't handle time zones. For this purpose, Java implements special classes that allow us to work with time taking into account different time zones. Those are the ZonedDateTime and OffsetDateTime date-time classes. In this topic, you will get acquainted with these classes, and learn about their purpose and how to operate with them. They are quite similar to the Instant class, so if you're familiar with this class, it will be easier to navigate this topic.

Classes describing time zones

Before exploring the classes mentioned above, let's first study the other three that allow them to work with time zones. Those classes are ZoneIdZoneRules, and ZoneOffset.

  • ZoneId is a class describing a time zone as a fixed offset, for instance, +04:00GMT+4 or UTC+04:00, or a region, such as Asia/Yerevan or Europe/London, etc. This class is related to the ZoneRules class that defines when and how the offset changes. The ZoneId class doesn't just show the current rules of the time zone but also past rules. For instance, as of 2022, Armenia doesn't do daylight saving time adjustments, but in 1999 it used to:
LocalDateTime pastSummerTime = LocalDateTime.of(1999, 9, 15, 13, 00);
LocalDateTime pastWinterTime = LocalDateTime.of(1999, 1, 15, 13, 00);

LocalDateTime summerTime2022 = LocalDateTime.of(2022, 9, 15, 13, 00);
LocalDateTime winterTime2022 = LocalDateTime.of(2022, 1, 15, 13, 00);

System.out.println(pastSummerTime.atZone(ZoneId.of("Asia/Yerevan"))); // 1999-09-15T13:00+05:00[Asia/Yerevan]
System.out.println(summerTime2022.atZone(ZoneId.of("Asia/Yerevan"))); // 2022-09-15T13:00+04:00[Asia/Yerevan]

System.out.println(pastWinterTime.atZone(ZoneId.of("Asia/Yerevan"))); // 1999-01-15T13:00+04:00[Asia/Yerevan]
System.out.println(winterTime2022.atZone(ZoneId.of("Asia/Yerevan"))); // 2022-01-15T13:00+04:00[Asia/Yerevan]

As you can see, there is a difference in the time zone in the two years. Now let's understand how the ZoneId class gets the information about time zones:

LocalDateTime  past = LocalDateTime.of(1999, 9, 15, 13, 00);
LocalDateTime  by2022 = LocalDateTime.of(2022, 9, 15, 13, 00);
ZoneRules  rules = ZoneId.of("Asia/Yerevan").getRules();

System.out.println(rules); // ZoneRules[currentStandardOffset=+04:00]
System.out.println("Fixed Offset: "  + rules.isFixedOffset()); // Fixed Offset: false
System.out.println("Past summer offset: "  + rules.getOffset(past)); // Past summer offset: +05:00
System.out.println("Current summer offset: "  + rules.getOffset(by2022)); // Current summer offset: +04:00

The example above clearly shows the purpose of the ZoneRules class. Inside the ZoneId class, there is a getRules() method returning a ZoneRules object, which gives us access to other ZoneRules methods.

  • ZoneOffset represents the fixed offset of the time zone. Its value can vary depending on the time of year if the specified region uses the daylight saving time approach.
ZoneOffset zoneOffset = ZoneOffset.of("+04:00");
ZoneOffset zoneOffsetHours = ZoneOffset.ofHours(4);
ZoneOffset zoneOffsetHoursMinutes = ZoneOffset.ofHoursMinutes(4, 30);

It extends ZoneId and describes the amount of time the given time zone differs from Greenwich time. Just like one region can have several offsets, one offset can represent several regions (countries, cities).
Note that there are regions where the offset contains not only hours but also minutes:

System.out.println(ZoneId.of("Iran").getRules()); // Iran ZoneRules[currentStandardOffset=+03:30]
System.out.println(ZoneId.of("Asia/Kolkata").getRules()); // ZoneRules[currentStandardOffset=+05:30]

Creating required objects

Now that you got acquainted with the classes providing operations with time zones, let's find out how to use them to create ZonedDateTime and OffsetDateTime units. You will explore three methods: of()from() and parse(). You are probably already familiar with them. These are common methods of the java.time package. The first method has many variations in the number and type of accepted parameters. We will consider some of them and you can easily explore others yourself.

LocalDate localDate = LocalDate.of(1991, 4, 15);
LocalTime localTime = LocalTime.of(18,30);
        
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
ZoneId zoneId = ZoneId.of("Asia/Yerevan");
ZoneOffset zoneOffset = ZoneOffset.of("+04:00");

System.out.println(ZonedDateTime.of(localDate, localTime, zoneId)); // 1991-04-15T18:30+04:00[Asia/Yerevan]
System.out.println(ZonedDateTime.ofInstant(Instant.EPOCH, zoneId)); // 1970-01-01T04:00+04:00[Asia/Yerevan]

System.out.println(OffsetDateTime.of(localDateTime, zoneOffset)); // 1991-04-15T18:30+04:00
System.out.println(OffsetDateTime.ofInstant(Instant.EPOCH, zoneId)); // 1970-01-01T04:00+04:00

As you can see, you can obtain the required units from an Instant object. A similar approach is implemented in the Instant class. It's possible to create its units by passing ZonedDateTime or OffsetDateTime objects to the Instant.from() method and obtain Instant units.
With the from() method, you can create objects of one class by passing as an argument another class object:

ZonedDateTime zonedDateTime1 = ZonedDateTime.from(OffsetDateTime.now());
ZonedDateTime zonedDateTime2 = ZonedDateTime.from(Instant.now());

OffsetDateTime offsetDateTime1 = OffsetDateTime.from(ZonedDateTime.now());
OffsetDateTime offsetDateTime2 = OffsetDateTime.from(LocalTime.now());

The parse() method behaves like the same method from the PeriodDuration, or Instant classes. It accepts text and parses it to the appropriate class instance.

ZonedDateTime.parse("1991-04-15T18:30+04:00[Asia/Yerevan]");
OffsetDateTime.parse("1970-01-01T04:00+04:00");

Performing operations

In this section, you will look at examples with various operational methods. You will understand how to use methods you know from the Instant class, as well as some other new methods.
Let's start by exploring how to compare units of these classes. The first approach is described below:

LocalDate localDate = LocalDate.of(1991, 4, 15);
LocalTime localTime = LocalTime.of(18,30);
        
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
ZoneId zoneId = ZoneId.of("Asia/Yerevan");
ZoneOffset zoneOffset = ZoneOffset.from(LocalDateTime.now().atZone(zoneId));

// unit1.toInstant().isBefore(unit2.toInstant())
ZonedDateTime.of(localDateTime, zoneId).isBefore(ZonedDateTime.of(LocalDateTime.now(), zoneId));
// unit1.toInstant().isAfter(unit2.toInstant())
ZonedDateTime.of(localDateTime, zoneId).isAfter(ZonedDateTime.of(LocalDateTime.now(), zoneId));

// unit1.toInstant().isBefore(unit2.toInstant())
OffsetDateTime.of(localDateTime, zoneOffset).isBefore(OffsetDateTime.of(LocalDateTime.now(), zoneOffset));
// unit1.toInstant().isAfter(unit2.toInstant())
OffsetDateTime.of(localDateTime, zoneOffset).isAfter(OffsetDateTime.of(LocalDateTime.now(), zoneOffset));

In the comments to the example above, you can see how the isBefore() and isAfter() methods actually compare objects. The other comparison method is the Comparable#compareTo() implementation which is probably familiar to you. For the ZonedDateTime class units it operates as expected, returning -10 or 1, but, when operating with OffsetDateTime units, it returns the difference in years:

LocalDate localDate1 = LocalDate.of(1991, 4, 15);
LocalTime localTime1 = LocalTime.of(18,30);

LocalDate localDate2 = LocalDate.of(1995, 5, 21);
LocalTime localTime2 = LocalTime.of(18,30);

LocalDateTime localDateTime1 = LocalDateTime.of(localDate1, localTime1);
LocalDateTime localDateTime2 = LocalDateTime.of(localDate2, localTime2);

ZoneOffset zoneOffset = ZoneOffset.of("+04:00");

OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime1, zoneOffset);
OffsetDateTime offsetDateTime2 = OffsetDateTime.of(localDateTime2, zoneOffset);

System.out.println(offsetDateTime1.compareTo(offsetDateTime2)); // -4
System.out.println(offsetDateTime2.compareTo(offsetDateTime1)); // 4

The next pair is also worth looking at. Both perform the equality comparison but in different ways:

LocalDate localDate = LocalDate.of(1991, 4, 15);
LocalTime localTime = LocalTime.of(18,30);
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);

ZoneId zoneId = ZoneId.of("Asia/Yerevan");
ZoneOffset zoneOffset = ZoneOffset.from(LocalDateTime.now().atZone(zoneId));

ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime, zoneOffset);
ZonedDateTime zonedDateTime2 = ZonedDateTime.of(localDateTime, zoneId);

System.out.println(zonedDateTime1.equals(zonedDateTime2)); // false
// unit1.toInstant().equals(unit2.toInstant())
System.out.println(zonedDateTime1.isEqual(zonedDateTime2)); // true

OffsetDateTime offsetDateTime1 = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0), ZoneOffset.of("+03:00"));
OffsetDateTime offsetDateTime2 = OffsetDateTime.ofInstant(Instant.EPOCH, zoneId);

System.out.println(offsetDateTime1.equals(offsetDateTime2)); // false
// unit1.toInstant().equals(unit2.toInstant())
System.out.println(offsetDateTime1.isEqual(offsetDateTime2)); // true

When comparing by the isEqual() method, you compare moments on the timeline where the time zone doesn't matter. As for comparing by equals(), it requires comparison by all the field values of the object, and you get a false result.
These two classes, like others from the same package, provide different variations of the get() method, such as getMonth()getHour()getSecond()getZone()getOffset(), and so on. You should already be familiar with them, so let's skip the code samples for those methods. The same can be said about the minus()plus() and until() methods.
In practice, you will face situations where you will need to get a date-time unit from another unit by changing the time zone.

ZoneId zone0 = ZoneId.of("GMT+0");
ZoneId londonZone = ZoneId.of("Europe/London");
ZoneId yerevanZone = ZoneId.of("Asia/Yerevan");
ZoneOffset offset0 = ZoneOffset.of("+00:00");
ZoneOffset londonOffset = ZoneOffset.of("+01:00");
ZoneOffset yerevanOffset = ZoneOffset.of("+04:00");
        
LocalDateTime localDateTime = LocalDateTime.of(1991, 4, 15, 13, 00);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, yerevanZone);
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, yerevanOffset);

System.out.println(zonedDateTime.withZoneSameInstant(zone0)); // 1991-04-15T09:00Z[GMT]
System.out.println(zonedDateTime.withZoneSameInstant(londonZone)); // 1991-04-15T10:00+01:00[Europe/London]
System.out.println(zonedDateTime.withZoneSameLocal(londonZone)); // 1991-04-15T13:00+01:00[Europe/London]

System.out.println(offsetDateTime.withOffsetSameInstant(offset0)); // 1991-04-15T09:00Z
System.out.println(offsetDateTime.withOffsetSameInstant(londonOffset)); // 1991-04-15T10:00+01:00
System.out.println(offsetDateTime.withOffsetSameLocal(yerevanOffset)); // 1991-04-15T13:00+04:00

For this purpose, you can use two methods. The withZoneSameInstant() returns the copy of the unit by calculating its instant and changing the time zone, and withZoneSameLocal() returns the copy of the same date and time but with a changed time zone.

Conclusion

The old java.util.Date class didn't represent any time zones, just a number of milliseconds since the Java epoch. Using it, developers often faced difficulties when working with time zones. Since Java 8, the new java.time additions have solved many issues that help developers handle operations involving time zones and daylight saving time. In this topic, we introduced you to the classes designed for that purpose,  ZonedDateTime and OffsetDateTime. These classes are especially important for working on applications where you must implement operations taking into account possible time zone changes.

Written by

Master Java by choosing your ideal learning course

View all courses

Create a free account to access the full topic

Sign up with Google
Sign up with Google
Sign up with JetBrains
Sign up with JetBrains
Sign up with Github
Sign up with GitHub
Coding thrill starts at Hyperskill
I've been using Hyperskill for five days now, and I absolutely love it compared to other platforms. The hands-on approach, where you learn by doing and solving problems, really accelerates the learning process.
Aryan Patil
Reviewed us on