A beginner’s guide to Java time zone handling

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Basic time notions

Most web applications have to support different time-zones and properly handling time-zones is no way easy. To make matters worse, you have to make sure that timestamps are consistent across various programming languages (e.g. JavaScript on the front-end, Java in the middle-ware and MongoDB as the data repository). This post aims to explain the basic notions of absolute and relative time.

Epoch

An epoch is a an absolute time reference. Most programming languages (e.g Java, JavaScript, Python) use the Unix epoch (Midnight 1 January 1970) when expressing a given timestamp as the number of milliseconds elapsed since a fixed point-in-time reference.

Relative numerical timestamp

The relative numerical timestamp is expressed as the number of milliseconds elapsed since the epoch.

Time zone

The coordinated universal time (UTC) is the most common time standard. The UTC time zone (equivalent to GMT) represents the time reference all other time zones relate to (through a positive/negative offset).

UTC time zone is commonly referred to as Zulu time (Z) or UTC+0. Japan’s time zone is UTC+9 and the Honolulu time zone is UTC-10. At the time of Unix epoch (1 January 1970 00:00 UTC time zone), it was 1 January 1970 09:00 in Tokyo and 31 December 1969 14:00 in Honolulu.

ISO 8601

ISO 8601 is the most widespread date/time representation standard and it uses the following date/time formats:

Time zone Notation
UTC 1970-01-01T00:00:00.000+00:00
UTC Zulu time 1970-01-01T00:00:00.000Z
Tokio 1970-01-01T09:00:00.000+09:00
Honolulu 1969-12-31T14:00:00.000-10:00

Java time basics

java.util.Date

java.util.Date is definitely the most common time-related class. It represents a fixed point in time, expressed as the relative number of milliseconds elapsed since epoch. java.util.Date is time zone independent, except for the toString method which uses a the local time zone for generating a String representation.

java.util.Calendar

The java.util.Calendar is both a Date/Time factory as well as a time zone aware timing instance. It’s one of the least user-friendly Java API class to work with and we can demonstrate this in the following example:

@Test
public void testTimeZonesWithCalendar() throws ParseException {
    assertEquals(
        0L, 
        newCalendarInstanceMillis("GMT").getTimeInMillis()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        newCalendarInstanceMillis("Japan").getTimeInMillis()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(10), 
        newCalendarInstanceMillis("Pacific/Honolulu").getTimeInMillis()
    );
    
    Calendar epoch = newCalendarInstanceMillis("GMT");
    epoch.setTimeZone(TimeZone.getTimeZone("Japan"));
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        epoch.getTimeInMillis()
    );
}

private Calendar newCalendarInstanceMillis(
        String timeZoneId) {
    Calendar calendar = new GregorianCalendar();
    calendar.set(Calendar.YEAR, 1970);
    calendar.set(Calendar.MONTH, 0);
    calendar.set(Calendar.DAY_OF_MONTH, 1);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    calendar.setTimeZone(TimeZone.getTimeZone(timeZoneId));
    return calendar;
}

At the time of Unix epoch (the UTC time zone), Tokyo time was nine hours ahead, while Honolulu was ten hours behind.

Changing a Calendar time zone preserves the actual time while shifting the zone offset. The relative timestamp changes along with the Calendar time zone offset.

Joda-Time and Java 8 Date Time API simply make java.util.Calandar obsolete so you no longer have to employ this quirky API.

org.joda.time.DateTime

Joda-Time aims to fix the legacy Date/Time API by offering:

With Joda-Time this is how our previous test case looks like:

@Test
public void testTimeZonesWithDateTime() throws ParseException {
    assertEquals(
        0L, 
        newDateTimeMillis("GMT").toDate().getTime()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(-9), 
        newDateTimeMillis("Japan").toDate().getTime()
    );
    
    assertEquals(
        TimeUnit.HOURS.toMillis(10), 
        newDateTimeMillis("Pacific/Honolulu").toDate().getTime()
    );
    
    DateTime epoch = newDateTimeMillis("GMT");
    
    assertEquals(
        "1970-01-01T00:00:00.000Z", 
        epoch.toString()
    );
    epoch = epoch.toDateTime(DateTimeZone.forID("Japan"));
    
    assertEquals(
        0, 
        epoch.toDate().getTime()
    );
    
    assertEquals(
        "1970-01-01T09:00:00.000+09:00", 
        epoch.toString()
    );
    
    MutableDateTime mutableDateTime = epoch.toMutableDateTime();
    mutableDateTime.setChronology(
        ISOChronology.getInstance().withZone(DateTimeZone.forID("Japan"))
    );
    assertEquals(
        "1970-01-01T09:00:00.000+09:00",
        epoch.toString()
    );
}


private DateTime newDateTimeMillis(
        String timeZoneId) {
    return new DateTime(DateTimeZone.forID(timeZoneId))
            .withYear(1970)
            .withMonthOfYear(1)
            .withDayOfMonth(1)
            .withTimeAtStartOfDay();
}

The DateTime fluent API is much easier to use than java.util.Calendar#set. DateTime is immutable but we can easily switch to a MutableDateTime if it’s appropriate for our current use case.

Compared to our Calendar test case, when changing the time zone the relative timestamp doesn’t change a bit, therefore remaining the same original point in time.

It’s only the human time perception that changes (1970-01-01T00:00:00.000Z and 1970-01-01T09:00:00.000+09:00 pointing to the very same absolute time).

Relative vs Absolute time instances

When supporting time zones, you basically have two main alternatives: a relative timestamp and absolute time info.

Relative timestamp

The numeric timestamp representation (the numbers of milliseconds since epoch) is relative info. This value is given against the UTC epoch but you still need a time zone to properly represent the actual time on a particular region.

Being a long value, it’s the most compact time representation and it’s ideal when exchanging huge amounts of data.

If you don’t know the original event time zone, you risk displaying a timestamp against the current local time zone and this is not always desirable.

Absolute timestamp

The absolute timestamp contains both the relative time as well as the time zone info. It’s quite common to express timestamps in their ISO 8601 string representation.

Compared to the numerical form (a 64 bit long) the string representation is less compact and it might take up to 25 characters (200 bits in UTF-8 encoding).

The ISO 8601 is quite common in XML files because the XML schema uses a lexical format inspired by the ISO 8601 standard.

Absolute time representation is much more convenient when we want to reconstruct the time instance against the original time zone. An e-mail client might want to display the email creation date using the sender’s time zone, and this can only be achieved using absolute timestamps.

Puzzles

The following exercise aims to demonstrate how difficult is to properly handle an ISO 8601 compliant date/time structure using the ancient java.text.DateFormat utilities.

java.text.SimpleDateFormat

First we are going to test the java.text.SimpleDateFormat parsing capabilities using the following test logic:

private void dateFormatParse(
        String pattern, 
        String dateTimeString, 
        long expectedNumericTimestamp) {
    try {
        Date utcDate = new SimpleDateFormat(pattern).parse(dateTimeString);
        if(expectedNumericTimestamp != utcDate.getTime()) {
            LOGGER.warn(
                "Pattern: {}, date: {} actual epoch {} while expected epoch: {}", 
                new Object[]{
                    pattern, 
                    dateTimeString, 
                    utcDate.getTime(), 
                    expectedNumericTimestamp
                }
            );
        }
    } catch (ParseException e) {
        LOGGER.warn(
            "Pattern: {}, date: {} threw {}", 
            new Object[]{
                pattern, 
                dateTimeString, 
                e.getClass().getSimpleName()
            }
        );
    }
}

Use case 1

Let’s see how various ISO 8601 patterns behave against this first parser:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

Yielding the following outcome:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSS'Z', 
date: 
    1970-01-01T00:00:00.200Z 
actual epoch -7199800 while expected epoch: 200

This pattern is not ISO 8601 compliant. The single quote character is an escape sequence so the final ‘Z’ symbol is not treated as a time directive (e.g. Zulu time). After parsing, we’ll simply get a local time zone Date reference.

This test was run using my current system default Europe/Athens time zone, which as of writing this post, it’s two hours ahead of UTC.

Use case 2

According to java.util.SimpleDateFormat documentation the following pattern: yyyy-MM-dd’T’HH:mm:ss.SSSZ should match an ISO 8601 date/time string value:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

But instead we got the following exception:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSSZ, 
date: 
    1970-01-01T00:00:00.200Z 
threw ParseException

So this pattern doesn’t seem to parse the Zulu time UTC string values.

Use case 3

The following patterns works just fine for explicit offsets:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200+0000", 
    200L
);

Use case 4

This pattern is also compatible with other time zone offsets:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 
    "1970-01-01T00:00:00.200+0100", 
    200L - 1000 * 60 * 60
);

Use case 5

To match the Zulu time notation we need to use the following pattern:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", 
    "1970-01-01T00:00:00.200Z", 
    200L
);

Use case 6

Unfortunately, this last pattern is not compatible with explicit time zone offsets:

dateFormatParse(
    "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", 
    "1970-01-01T00:00:00.200+0000", 
    200L
);

Ending-up with the following exception:

Pattern: 
    yyyy-MM-dd'T'HH:mm:ss.SSSXXX,
date: 
    1970-01-01T00:00:00.200+0000 
threw ParseException

org.joda.time.DateTime

As opposed to java.text.SimpleDateFormat, Joda-Time is compatible with any ISO 8601 pattern. The following test case is going to be used for the upcoming test cases:

private void jodaTimeParse(
        String dateTimeString, 
        long expectedNumericTimestamp) {
    Date utcDate = DateTime.parse(dateTimeString).toDate();
    if(expectedNumericTimestamp != utcDate.getTime()) {
        LOGGER.warn(
            "date: {} actual epoch {} while expected epoch: {}", 
            new Object[]{
                dateTimeString, 
                utcDate.getTime(), 
                expectedNumericTimestamp
            }
        );
    }
}

Joda-Time is compatible with all standard ISO 8601 date/time formats:

jodaTimeParse(
    "1970-01-01T00:00:00.200Z", 
    200L
);

jodaTimeParse(
    "1970-01-01T00:00:00.200+0000", 
    200L
);

jodaTimeParse(
    "1970-01-01T00:00:00.200+0100", 
    200L - 1000 * 60 * 60
);

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As you can see, the ancient Java Date/Time utilities are not easy to work with. Joda-Time is a much better alternative, offering better time handling features.

If you happen to work with Java 8, it’s worth switching to the Java 8 Date/Time API that is very much based on Joda-Time.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.