Java Joda time – the danger of time zone conversion
At work, I wrote some time zone conversion code in Java and was surprised to learn of the dangers of using Joda time library. It throws an exception during 1 hour ‘error window’when clocks are moved forward 1 hour eg Daylight Savings Start (DST). Our legacy code used Joda time library.
Lessons learnt
Avoid time zone conversion code where possible. If need to convert time, use java.time rather than joda time
Use LocalDateTime if possible
Or use UTC if LocalDateTime not possible
Joda Time (version 2.10.6)
Daylight savings end (Sun 5 Apr 2020) – 3am clock moves back 1 hour was ok Daylight savings start (Sun 4 Oct 2020) – 2am clock moves forward 1 hour has 1 hour error window where it throws an exception
SYD time
Joda time SYD
Joda time UTC
Hours diff
04/10/20 01:59:59
04/10/20 01:59:59
03/10/20 15:59:59
10
04/10/20 02:00:00
04/10/20 02:00:00
Illegal instant due to (see below)
n/a
04/10/20 02:59:59
04/10/20 02:59:59
Illegal instant due to(see below)
n/a
04/10/20 03:00:00
04/10/20 03:00:00
03/10/20 16:00:00
11
Exception = Illegal instant due to time zone offset transition (daylight savings time ‘gap‘): 2020-10-04T02:00:00.000 (Australia/Sydney)
Java Time
Luckily the java.time library available from Java 8 works as expected, correctly resolving the ‘error window’ and not throwing any exceptions
SYD time
Java time SYD
Java time UTC
Hours diff
04/10/20 01:59:59
04/10/20 01:59:59
03/10/20 15:59:59
10
04/10/20 02:00:00
04/10/20 03:00:00
03/10/20 16:00:00
11
04/10/20 02:59:59
04/10/20 03:59:59
03/10/20 16:59:59
11
04/10/20 03:00:00
04/10/20 03:00:00
03/10/20 16:00:00
11
04/10/20 03:59:59
04/10/20 03:59:59
03/10/20 16:59:59
11
Joda Time test code
I discovered this issue when writing some tests
@Test
void convertSydTimeToUtc_daylightSavingsStarts_beforeCutoff() {
LocalDateTime localDateTime = new LocalDateTime(2020, 10, 4, 1, 59, 59);
DateTime sydDateTime = localDateTime.toDateTime(sydneyTimeZone);
DateTime utcDateTime = TimezoneConverter.convertSydToUtc(sydDateTime);
assertThat(utcDateTime.toString()).isEqualTo("2020-10-03T15:59:59.000Z");
int offsetInMillis = sydneyTimeZone.getOffset(sydDateTime.toInstant());
int offsetInHours = offsetInMillis / HOURS_IN_MILLIS;
assertThat(offsetInHours).isEqualTo(10);
}
@Test
void daylightSavingsStart_forward1Hour_throwsException_duringDeadWindowStart() {
LocalDateTime localDateTime = new LocalDateTime(2020, 10, 4, 2, 0, 0);
assertThatThrownBy(() -> localDateTime.toDateTime(sydneyTimeZone))
.isInstanceOf(IllegalInstantException.class)
.hasMessageContaining("Illegal instant due to time zone offset transition (daylight savings time 'gap')");
}