I am not surprised you've found issues with FastDateFormat. I'd be more surprised if you didn't. Handling time zones is *hard* and almost everyone gets it wrong, at least at first. In 2025 I wouldn't use either SimpleDateFormat or FastDateFormat. Use java.time and java.time.formatter instead. These packages are thread safe, and have been through much more rigorous analysis, testing, and design by real time zone experts. It's probably time to deprecate FastDateFormat in favor of java.time.
On Tue, Feb 11, 2025 at 9:47 AM Matthew Bellew <matth...@labkey.com.invalid> wrote: > > 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 > } > } -- Elliotte Rusty Harold elh...@ibiblio.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org For additional commands, e-mail: dev-h...@commons.apache.org