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 ZoneId
, ZoneRules
, and ZoneOffset
.
ZoneId
is a class describing a time zone as a fixed offset, for instance,+04:00
,GMT+4
orUTC+04:00
, or a region, such asAsia/Yerevan
orEurope/London
, etc. This class is related to theZoneRules
class that defines when and how the offset changes. TheZoneId
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 Period
, Duration
, 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 -1
, 0
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.