Android SimpleDateFormat format timezones in a twist

Yesterday we found a problem regarding formatting dates including Time Zones on Android 2.1 and 2.2 devices.  In a few cases our unit tests were failing due to the dates being output with the wrong timezone identifer.

Specifically it turns out that two identical calls to format a date will return different output if a date including a timezone is parsed using the java.text.SimpleDateFormat instance inbetween the two calls.

The following JUnit.org - The JUnit testing framework tests pass

public void testFormatAfterParse() throws Exception {
    Date aDate = new Date(1311033202747L);
    SimpleDateFormat sdf = new SimpleDateFormat(
                     "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    assertEquals("2011-07-18T23:53:22.747+0000",
                     sdf.format(aDate));
    assertEquals("2011-07-18T23:53:22.747+0000",
                    sdf.format(aDate));
}

whereas if we parse a date between the two we get a test failure

public void testFormatAfterParse() throws Exception {
    Date aDate = new Date(1311033202747L);
    SimpleDateFormat sdf = new SimpleDateFormat(
                     "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    assertEquals("2011-07-18T23:53:22.747+0000",
                     sdf.format(aDate));
    <strong>sdf.parse("2009-01-01T00:00:00.000+0200"); </strong>        // fails on the next line
    assertEquals("2011-07-18T23:53:22.747+0000",
                     sdf.format(aDate));
}

With the additional sdf.parse call the Junit test fails saying

junit.framework.ComparisonFailure:
      expected:<...0...> but was:<...2...>

That is the timezone output said “+0200” and not the expected “+0000” output.

This problem seems to have been fixed in Android 2.3 there is a fleeting reference to it in Comment 21 of Android issue 8258 (SimpleDateFormat timezone problem with 2.1) but I have not found an specific bug report for it yet.

After quite a bit of investigation we managed to identify the test case above but it was difficult to identify the exact scenario in our multi-threaded environment where SimpleDateFormat instances are used within a utility class that is called from many different contexts.

Once identified the problem is simple to workaround in our application. We have just allocated two separate SimpleDateFormat instances (actually 2 per thread – more below) one for formatting and one for parsing. This stops parsing from affecting the format calls output.

Sharing SimpleDateFormat instances

You may ask why we are reusing SimpleDateFormat instances and hence falling over this problem. This is due to the cost involved in allocating a new instance. When we were using a new instance everytime we wanted to format/parse a date our tests took 12 minutes to complete. Changing to use a per-thread instance of SimpleDateFormat as shown below the test time was reduced by 9 minutes so it now completes in just 3 minutes.

Ok so we do a large number of date time operations in the tests but it really does show how much time the allocation of the object takes.

We use a java.lang.ThreadLocal helper to allocate new SimpleDateFormat objects. Our original formatting code was

public static String formatAsIso8601XmlLocal(Date datetime) {
    SimpleDateFormat sdf = new SimpleDateFormat(
            "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    return sdf.format(datetime);
}

but with a simple use of ThreadLocal we can safely reuse instances in a thread-safe manner with minimal effort.

/** Zulu timezone date/time format helper. */
private static final ThreadLocal<SimpleDateFormat>
                         iso8601XmlZuluDateFormatHelper =
    new ThreadLocal<SimpleDateFormat>() {
        /* (non-Javadoc)
         * @see java.lang.ThreadLocal#initialValue()
         */
        @Override
        protected SimpleDateFormat initialValue() {
            // Note Locale.US is always available
            // and formats correctly for XML interchange
            SimpleDateFormat sdf = new SimpleDateFormat(
                   "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
            return sdf;
        }
    };
}

public static String formatAsIso8601XmlLocal(Date datetime) {
    return iso8601XmlLocalDateFormatHelper.get()
                                      .format(datetime);
}