I started using FastDateFormat.parser() while expanding some internal tests
of an old custom date parser, and killing usages of
SimpleDateFormat.parse().  Since FastDateFormat uses Calendar internally to
keep state, I was surprised that I got unexpected results when using
"generic" timezones.  Generic as opposed to explicitly "standard" or
"daylight" time zones.

To add somewhat to the confusion, while European time zones have a summer
designation they don't have an explicit standard designation. For instance,
"CET" is the "generic" short name for the Central European Time and "CEST"
is "Central European Summer Time".  There is no "standard" time
designation. Our European users made this quite clear to us.  CET should
behave like "Pacific Time" not like "PST".

The specific problem I observed is that "generic" names always act like
standard time.  Since FastDateFormat uses Calendar and TimeZone internally
this surprised me.  The included below shows what I mean.  This test code
"passes" showing that Calendar and FastDateFormat do not translate dates
consistently.  The problem seems to be that FastDateFormat (specifically
FastDateParser.TimeZoneStrategy) always turns a TimeZone into a
TimeOffset.  Since it does this before the date is parsed, it can't know
whether it should be using the DST offset or not.

I imagine that the alternative might be to pass the TimeZone to the
Calendar and let it do it's thing, unless the TimeZone designation exactly
matches the short or long daylight savings name.  However, I am aware
dealing with dates always has gotchas.  Just hoping that these observations
are useful for you.


package org.labkey.api.util;

import org.apache.commons.lang3.time.FastDateFormat;
import org.junit.Assert;
import org.junit.Test;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class DateFormatTestCase extends Assert
{
    int gmtHour(long ms)
    {
        return (int) ((ms / (60 * 60 * 1000L)) % 24);
    }

    long calendarNoon(String tzName, int month)
    {
        var g = new GregorianCalendar(TimeZone.getTimeZone(tzName));
        g.set(Calendar.YEAR, 2020);
        g.set(Calendar.MONTH, month);
        g.set(Calendar.DAY_OF_MONTH, 1);
        g.set(Calendar.HOUR_OF_DAY, 12);
        return g.getTimeInMillis();
    }

    @Test
    public void testSimpleDateParse() throws Exception
    {
        // given the above, we expect hours field to change based on date
for EET, CET, WET
        assertEquals(20, gmtHour(calendarNoon("America/Los_Angeles",
Calendar.JANUARY)));       // winter
        assertEquals(19, gmtHour(calendarNoon("America/Los_Angeles",
Calendar.JULY)));          // summer

        assertEquals(10, gmtHour(calendarNoon("EET", Calendar.JANUARY)));
                    // winter
        assertEquals( 9, gmtHour(calendarNoon("EET", Calendar.JULY)));
                     // summer

        assertEquals(11, gmtHour(calendarNoon("CET", Calendar.JANUARY)));
                    // winter
        assertEquals(10, gmtHour(calendarNoon("CET", Calendar.JULY)));
                     // summer

        assertEquals(12, gmtHour(calendarNoon("WET", Calendar.JANUARY)));
                    // winter
        assertEquals(11, gmtHour(calendarNoon("WET", Calendar.JULY)));
                     // summer

        // If we use FastDateFormat the times _do_not_change_ as we export
(or at least hope)
        var fastFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm
(zzz)");

        assertEquals(20, gmtHour(fastFormat.parse("2020-01-01 12:00
(Pacific Time)").getTime()));  // winter
        assertEquals(20, gmtHour(fastFormat.parse("2020-07-01 12:00
(Pacific Time)").getTime()));  // summer

        assertEquals(10, gmtHour(fastFormat.parse("2020-01-01 12:00
(EET)").getTime()));        // winter
        assertEquals(10, gmtHour(fastFormat.parse("2020-07-01 12:00
(EET)").getTime()));        // summer

        assertEquals(11, gmtHour(fastFormat.parse("2020-01-01 12:00
(CET)").getTime()));        // winter
        assertEquals(11, gmtHour(fastFormat.parse("2020-07-01 12:00
(CET)").getTime()));        // summer

        assertEquals(12, gmtHour(fastFormat.parse("2020-01-01 12:00
(WET)").getTime()));        // winter
        assertEquals(12, gmtHour(fastFormat.parse("2020-07-01 12:00
(WET)").getTime()));        // summer
    }
}

Reply via email to