From 34beef1d085c4571b8b8924951043276e1cc29f9 Mon Sep 17 00:00:00 2001 From: Sebastian Bazley Date: Tue, 25 Sep 2012 16:32:20 +0000 Subject: [PATCH] LANG-828 FastDateParser does not handle non-Gregorian calendars properly git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@1389976 13f79535-47bb-0310-9956-ffa450edef68 --- src/changes/changes.xml | 1 + .../commons/lang3/time/FastDateParser.java | 52 ++++++++++++++++++- .../lang3/time/FastDateParserTest.java | 52 +++++++++++++++---- 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2cb636e32..82b036ae3 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,7 @@ + FastDateParser does not handle non-Gregorian calendars properly FastDateParser does not handle non-ASCII digits correctly Create StrBuilder APIs similar to String.format(String, Object...) Add org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS_8 diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java index 03b883f4d..1776164e3 100644 --- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java +++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java @@ -22,6 +22,7 @@ import java.text.DateFormatSymbols; import java.text.ParseException; import java.text.ParsePosition; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -55,6 +56,13 @@ *

Timing tests indicate this class is as about as fast as SimpleDateFormat * in single thread applications and about 25% faster in multi-thread applications.

* + *

Note that the code only handles Gregorian calendars. The following non-Gregorian + * calendars use SimpleDateFormat internally, and so will be slower: + *

+ *

* @since 3.2 */ public class FastDateParser implements DateParser, Serializable { @@ -114,7 +122,17 @@ private void init() { if(!patternMatcher.lookingAt()) { throw new IllegalArgumentException("Invalid pattern"); } - + + String localeName = locale.toString(); + // These locales don't use the Gregorian calendar + // See http://docs.oracle.com/javase/6/docs/technotes/guides/intl/calendar.doc.html + if (localeName.equals("ja_JP_JP") || localeName.startsWith("th_TH")) { + collector.add(new SimpleDateFormatStrategy()); + strategies= collector.toArray(new Strategy[collector.size()]); + parsePattern= Pattern.compile("(.*+)"); + return; + } + currentFormatField= patternMatcher.group(); Strategy currentStrategy= getStrategy(currentFormatField); for(;;) { @@ -797,6 +815,38 @@ else if(value.startsWith("GMT")) { } } + + /** + * Dummy strategy which delegates to SimpleDateFormat. + */ + private static class SimpleDateFormatStrategy implements Strategy { + + @Override + public boolean isNumber() { + return false; + } + + @Override + public void setCalendar(FastDateParser parser, Calendar cal, String value) { + String pat = parser.pattern; + Locale loc = parser.locale; + SimpleDateFormat sdf = new SimpleDateFormat(pat, loc); + try { + Date d = sdf.parse(value); + cal.setTime(d); + } catch (ParseException e) { + throw new IllegalArgumentException( + "Unexpected error using pattern " + pat + " with locale " + loc.toString(), e); + } + } + + @Override + public boolean addRegex(FastDateParser parser, StringBuilder regex) { + return false; + } + + } + private static final Strategy ERA_STRATEGY = new TextStrategy(Calendar.ERA); private static final Strategy DAY_OF_WEEK_STRATEGY = new TextStrategy(Calendar.DAY_OF_WEEK); private static final Strategy AM_PM_STRATEGY = new TextStrategy(Calendar.AM_PM); diff --git a/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java b/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java index 6cdb46977..80cbf6df2 100644 --- a/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java +++ b/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; - import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -31,6 +30,8 @@ import java.util.Map; import java.util.TimeZone; +import junit.framework.Assert; + import org.apache.commons.lang3.SerializationUtils; import org.junit.Test; @@ -191,15 +192,37 @@ public void testAmPm() throws ParseException { public void testParses() throws Exception { for(Locale locale : Locale.getAvailableLocales()) { for(TimeZone tz : new TimeZone[]{NEW_YORK, GMT}) { - Calendar cal = Calendar.getInstance(tz); - cal.clear(); - cal.set(2003, 1, 10); - Date in = cal.getTime(); - for(String format : new String[]{LONG_FORMAT, SHORT_FORMAT}) { - SimpleDateFormat sdf = new SimpleDateFormat(LONG_FORMAT, locale); - String fmt = sdf.format(in); - Date out = sdf.parse(fmt); - assertEquals(locale.toString()+" "+ format+ " "+tz.getID(), in, out); + Calendar cal = Calendar.getInstance(tz); + for(int year : new int[]{2003, 1940, 1868, 1867, 0, -1940}) { + // http://docs.oracle.com/javase/6/docs/technotes/guides/intl/calendar.doc.html + if (year < 1868 && locale.toString().equals("ja_JP_JP")) { + continue; // Japanese imperial calendar does not support eras before 1868 + } + cal.clear(); + if (year < 0) { + cal.set(-year, 1, 10); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(year, 1, 10); + } + Date in = cal.getTime(); + for(String format : new String[]{LONG_FORMAT, SHORT_FORMAT}) { + SimpleDateFormat sdf = new SimpleDateFormat(format, locale); + if (format.equals(SHORT_FORMAT)) { + if (year < 1930) { + sdf.set2DigitYearStart(cal.getTime()); + } + } + String fmt = sdf.format(in); + try { + Date out = sdf.parse(fmt); + + assertEquals(locale.toString()+" "+year+" "+ format+ " "+tz.getID(), in, out); + } catch (ParseException pe) { + System.out.println(fmt+" "+locale.toString()+" "+year+" "+ format+ " "+tz.getID()); + throw pe; + } + } } } } @@ -253,19 +276,28 @@ private void testLocales(String format, boolean eraBC) throws Exception { if (eraBC) { cal.set(Calendar.ERA, GregorianCalendar.BC); } + boolean failed = false; for(Locale locale : Locale.getAvailableLocales()) { + // ja_JP_JP cannot handle dates before 1868 properly + if (eraBC && format.equals(SHORT_FORMAT) && locale.toString().equals("ja_JP_JP")) { + continue; + } SimpleDateFormat sdf = new SimpleDateFormat(format, locale); DateParser fdf = getInstance(format, locale); try { checkParse(locale, cal, sdf, fdf); } catch(ParseException ex) { + failed = true; // TODO: are these Java bugs? // ja_JP_JP, th_TH, and th_TH_TH fail with both eras because the generated era name does not match // ja_JP_JP fails with era BC because it converts to -2002 System.out.println("Locale "+locale+ " failed with "+format+" era "+(eraBC?"BC":"AD")+"\n" + trimMessage(ex.toString())); } } + if (failed) { + Assert.fail("One or more tests failed, see above"); + } } private String trimMessage(String msg) {