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 } }