Daylight Saving Time, or DST, is a practice of advancing clocks during summer months in order to leverage an additional hour of the natural light (saving heating power, illumination power, enhancing the mood, and so on).
It's used by several countries and needs to be taken into account when working with dates and timestamps.
In this tutorial, we'll see how to correctly handle DST in Java according to different locations.
2. JRE and DST Mutability
First, it's extremely important to understand that worldwide DST zones change very often and there's no central authority coordinating it.
A country, or in some cases even a city, can decide if and how to apply or revoke it.
In case it's not possible to wait, we can force the modified Time Zone data containing the new DST settings into the JRE through an official Oracle tool called Java Time Zone Updater Tool, available on the Java SE download page.
3. The Wrong Way: Three-Letter Timezone ID
Back in the JDK 1.1 days, the API allowed three-letter time zone IDs, but this led to several problems.
First, this was because the same three-letter ID could refer to multiple time zones. For example, CST could be U.S. “Central Standard Time”, but also “China Standard Time”. The Java platform could then only recognize one of them.
Another issue was that Standard timezones never take Daylight Saving Time into an account. Multiple areas/regions/cities can have their local DST inside the same Standard time zone, so the Standard time doesn't observe it.
Due to backward compatibility, it's still possible to instantiate a java.util.Timezone with a three-letter ID. However, this method is deprecated and shouldn't be used anymore.
4. The Right Way: TZDB Timezone ID
The right way to handle DST in Java is to instantiate a Timezone with a specific TZDB Timezone ID, eg. “Europe/Rome”.
Then, we'll use this in conjunction with time-specific classes like java.util.Calendar to get a proper configuration of the TimeZone's raw offset (to the GMT time zone), and automatic DST shift adjustments.
Let's see how the shift from GMT+1 to GMT+2 (which happens in Italy on March 25, 2018, at 02:00 am) is automatically handled when using the right TimeZone:
TimeZone tz = TimeZone.getTimeZone("Europe/Rome"); TimeZone.setDefault(tz); Calendar cal = Calendar.getInstance(tz, Locale.ITALIAN); DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ITALIAN); Date dateBeforeDST = df.parse("2018-03-25 01:55"); cal.setTime(dateBeforeDST); assertThat(cal.get(Calendar.ZONE_OFFSET)).isEqualTo(3600000); assertThat(cal.get(Calendar.DST_OFFSET)).isEqualTo(0);
As we can see, ZONE_OFFSET is 60 minutes (because Italy is GMT+1) while DST_OFFSET is 0 at that time.
Let's add ten minutes to the Calendar:
Now DST_OFFSET has become 60 minutes too, and the country has transitioned its local time from CET (Central European Time) to CEST (Central European Summer Time) which is GMT+2:
Date dateAfterDST = cal.getTime(); assertThat(cal.get(Calendar.DST_OFFSET)) .isEqualTo(3600000); assertThat(dateAfterDST) .isEqualTo(df.parse("2018-03-25 03:05"));
If we display the two dates in the console, we'll see the time zone change as well:
Before DST (00:55 UTC - 01:55 GMT+1) = Sun Mar 25 01:55:00 CET 2018 After DST (01:05 UTC - 03:05 GMT+2) = Sun Mar 25 03:05:00 CEST 2018
As a final test, we can measure the distance between the two Dates, 1:55 and 3:05:
Long deltaBetweenDatesInMillis = dateAfterDST.getTime() - dateBeforeDST.getTime(); Long tenMinutesInMillis = (1000L * 60 * 10); assertThat(deltaBetweenDatesInMillis) .isEqualTo(tenMinutesInMillis);
As we'd expect, the distance is of 10 minutes instead of 70.
We've seen how to avoid falling into the common pitfalls that we can encounter when working with Date through the correct usage of TimeZone and Locale.
5. The Best Way: Java 8 Date/Time API
Working with these thread-unsafe and not always user-friendly java.util classes have always been tough, especially due to compatibility concerns which prevented them from being properly refactored.
For this reason, Java 8 introduced a brand new package, java.time, and a whole new API set, the Date/Time API. This is ISO-centric, fully thread-safe and heavily inspired by the famous library Joda-Time.
Let's take a closer look at this new classes, starting from the successor of java.util.Date, java.time.LocalDateTime:
LocalDateTime localDateTimeBeforeDST = LocalDateTime .of(2018, 3, 25, 1, 55); assertThat(localDateTimeBeforeDST.toString()) .isEqualTo("2018-03-25T01:55");
We can observe how a LocalDateTime is conforming to the ISO8601 profile, a standard and widely adopted date-time notation.
It's completely unaware of Zones and Offsets, though, that's why we need to convert it into a fully DST-aware java.time.ZonedDateTime:
ZoneId italianZoneId = ZoneId.of("Europe/Rome"); ZonedDateTime zonedDateTimeBeforeDST = localDateTimeBeforeDST .atZone(italianZoneId); assertThat(zonedDateTimeBeforeDST.toString()) .isEqualTo("2018-03-25T01:55+01:00[Europe/Rome]");
As we can see, now the date incorporates two fundamental trailing pieces of information: +01:00 is the ZoneOffset, while [Europe/Rome] is the ZoneId.
Like in the previous example, let's trigger DST through the addition of ten minutes:
ZonedDateTime zonedDateTimeAfterDST = zonedDateTimeBeforeDST .plus(10, ChronoUnit.MINUTES); assertThat(zonedDateTimeAfterDST.toString()) .isEqualTo("2018-03-25T03:05+02:00[Europe/Rome]");
Again, we see how both the time and the zone offset are shifting forward, and still keeping the same distance:
Long deltaBetweenDatesInMinutes = ChronoUnit.MINUTES .between(zonedDateTimeBeforeDST,zonedDateTimeAfterDST); assertThat(deltaBetweenDatesInMinutes) .isEqualTo(10);
We've seen what Daylight Saving Time is and how to handle it through some practical examples in different versions of Java core API.
When working with Java 8 and above, the usage of the new java.time package is encouraged thanks to the ease of use and to its standard, thread-safe nature.
As always, the full source code is available over on Github.