diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DateFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DateFormat.java index f83fc3c4197..d8eda16b4e7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DateFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DateFormat.java @@ -452,6 +452,8 @@ public abstract class DateFormat extends UFormat { * PARSE_ALLOW_WHITESPACE - indicates whitespace tolerance. Also included is trailing dot tolerance. *
* PARSE_ALLOW_NUMERIC - indicates tolerance of numeric data when String data may be assumed. eg: YEAR_NAME_FIELD + *
+ * PRASE_PARTIAL_MATCH - indicates tolerance of partial matches against pattern literals * * @internal ICU technology preview */ @@ -465,7 +467,12 @@ public abstract class DateFormat extends UFormat { * indicates tolerance of numeric data when String data may be assumed. eg: YEAR_NAME_FIELD * @internal ICU technology preview */ - PARSE_ALLOW_NUMERIC + PARSE_ALLOW_NUMERIC, + /** + * indicates tolerance of a partial literal match + * @draft ICU 53 + */ + PARSE_PARTIAL_MATCH }; /** @@ -1472,7 +1479,7 @@ public abstract class DateFormat extends UFormat { return calendar.isLenient(); } - /** + /** * set a boolean attribute for this instance. Aspects of DateFormat leniency are controlled by * boolean attributes. * diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java index be8470858e2..f3513e43add 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java @@ -2438,6 +2438,9 @@ public class SimpleDateFormat extends DateFormat { } else if ((pch == ' ' || pch == '.') && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_WHITESPACE)) { ++idx; continue; + } else if (pos != originalPos && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_MATCH)) { + ++idx; + continue; } break; } @@ -2731,11 +2734,14 @@ public class SimpleDateFormat extends DateFormat { if (patternCharIndex == 4 /*'k' HOUR_OF_DAY1_FIELD*/ || patternCharIndex == 15 /*'h' HOUR1_FIELD*/ || (patternCharIndex == 2 /*'M' MONTH_FIELD*/ && count <= 2) || - (patternCharIndex == 26 /*'L' STAND_ALONE_MONTH*/ && count <= 2) || - (patternCharIndex == 19 /*'e' DOW_LOCAL*/ && count <= 2) || + patternCharIndex == 26 /*'L' STAND_ALONE_MONTH*/ || + patternCharIndex == 19 /*'e' DOW_LOCAL*/ || + patternCharIndex == 25 /*'c' STAND_ALONE_DAY_OF_WEEK*/ || patternCharIndex == 1 /*'y' YEAR */ || patternCharIndex == 18 /*'Y' YEAR_WOY */ || patternCharIndex == 30 /*'U' YEAR_NAME_FIELD, falls back to numeric */ || (patternCharIndex == 0 /*'G' ERA */ && isChineseCalendar) || + patternCharIndex == 27 /* 'Q' - QUARTER*/ || + patternCharIndex == 28 /* 'q' - STANDALONE QUARTER*/ || patternCharIndex == 8 /*'S' FRACTIONAL_SECOND */ ) { // It would be good to unify this with the obeyCount logic below, @@ -2764,7 +2770,8 @@ public class SimpleDateFormat extends DateFormat { } else { number = parseInt(text, pos, allowNegative,currentNumberFormat); } - if (number == null && patternCharIndex != 30) { + if (number == null && !allowNumericFallback(patternCharIndex)) { + // only return if pattern is NOT one that allows numeric fallback return ~start; } } @@ -2853,7 +2860,8 @@ public class SimpleDateFormat extends DateFormat { return ~start; case 2: // 'M' - MONTH case 26: // 'L' - STAND_ALONE_MONTH - if (count <= 2) { // i.e., M/MM, L/LL + if (count <= 2 || (number != null && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) { + // i.e., M/MM, L/LL or lenient & have a number // Don't want to parse the month if it is a string // while pattern uses numeric style: M/MM, L/LL. // [We computed 'value' above.] @@ -2918,7 +2926,8 @@ public class SimpleDateFormat extends DateFormat { cal.set(Calendar.MILLISECOND, value); return pos.getIndex(); case 19: // 'e' - DOW_LOCAL - if(count <= 2) { // i.e. e/ee + if(count <= 2 || (number != null && (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) ) { + // i.e. e/ee or lenient and have a number cal.set(field, value); return pos.getIndex(); } @@ -2946,6 +2955,11 @@ public class SimpleDateFormat extends DateFormat { return newStart; } case 25: { // 'c' - STAND_ALONE_DAY_OF_WEEK + if(count == 1 || (number != null && (getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) ) { + // i.e. c or lenient and have a number + cal.set(field, value); + return pos.getIndex(); + } // Want to be able to parse at least wide, abbrev, short forms. int newStart = matchString(text, start, Calendar.DAY_OF_WEEK, formatData.standaloneWeekdays, null, cal); // try cccc wide if (newStart > 0) { @@ -3100,7 +3114,8 @@ public class SimpleDateFormat extends DateFormat { return ~start; } case 27: // 'Q' - QUARTER - if (count <= 2) { // i.e., Q or QQ. + if (count <= 2 || (number != null && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) { + // i.e., Q or QQ. or lenient & have number // Don't want to parse the quarter if it is a string // while pattern uses numeric style: Q or QQ. // [We computed 'value' above.] @@ -3121,7 +3136,8 @@ public class SimpleDateFormat extends DateFormat { } case 28: // 'q' - STANDALONE QUARTER - if (count <= 2) { // i.e., q or qq. + if (count <= 2 || (number != null && getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC))) { + // i.e., q or qq. or lenient & have number // Don't want to parse the quarter if it is a string // while pattern uses numeric style: q or qq. // [We computed 'value' above.] @@ -3170,6 +3186,22 @@ public class SimpleDateFormat extends DateFormat { } } + /** + * return true if the pattern specified by patternCharIndex is one that allows + * numeric fallback regardless of actual pattern size. + */ + private boolean allowNumericFallback(int patternCharIndex) { + if (patternCharIndex == 26 /*'L' STAND_ALONE_MONTH*/ || + patternCharIndex == 19 /*'e' DOW_LOCAL*/ || + patternCharIndex == 25 /*'c' STAND_ALONE_DAY_OF_WEEK*/ || + patternCharIndex == 30 /*'U' YEAR_NAME_FIELD*/ || + patternCharIndex == 27 /* 'Q' - QUARTER*/ || + patternCharIndex == 28 /* 'q' - STANDALONE QUARTER*/) { + return true; + } + return false; + } + /** * Parse an integer using numberFormat. This method is semantically * const, but actually may modify fNumberFormat. diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatRegressionTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatRegressionTest.java index 68f6bbcb76a..69c8dfde94c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatRegressionTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatRegressionTest.java @@ -1275,5 +1275,80 @@ public class DateFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { } } } - + + + public void TestT10334() { + String pattern = new String("'--: 'EEE-WW-MMMM-yyyy"); + String text = new String("--mon-02-march-2011"); + SimpleDateFormat format = new SimpleDateFormat(pattern); + + format.setBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_MATCH, false); + try { + format.parse(text); + errln("parse partial match did NOT fail in strict mode!"); + } catch (ParseException pe) { + // expected + } + + format.setBooleanAttribute(DateFormat.BooleanAttribute.PARSE_PARTIAL_MATCH, true); + try { + format.parse(text); + } catch (ParseException pe) { + errln("parse partial match failure in lenient mode: " + pe.getLocalizedMessage()); + } + + pattern = new String("YYYY MM dd"); + text = new String("2013 12 10"); + format.applyPattern(pattern); + Date referenceDate = null; + try { + referenceDate = format.parse(text); + } catch (ParseException pe) { + errln("unable to instantiate reference date: " + pe.getLocalizedMessage()); + } + + FieldPosition fp = new FieldPosition(0); + pattern = new String("YYYY LL dd ee cc qq QQ"); + format.applyPattern(pattern); + StringBuffer formattedString = new StringBuffer(); + formattedString = format.format(referenceDate, formattedString, fp); + logln("ref date: " + formattedString); + + + pattern = new String("YYYY LLL dd eee ccc qqq QQQ"); + text = new String("2013 12 10 03 3 04 04"); + format.applyPattern(pattern); + logln(format.format(referenceDate)); + + format.setBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC, true); + ParsePosition pp = new ParsePosition(0); + format.parse(text, pp); + int errorIdx = pp.getErrorIndex(); + if (errorIdx != -1) { + + errln("numeric parse error at["+errorIdx+"] on char["+pattern.substring(errorIdx, errorIdx+1)+"] in pattern["+pattern+"]"); + } + + format.setBooleanAttribute(DateFormat.BooleanAttribute.PARSE_ALLOW_NUMERIC, false); + try { + format.parse(text); + errln("numeric parse did NOT fail in strict mode!"); + } catch (ParseException pe) { + // expected + } + + /* + * test to verify new code (and improve code coverage) for normal quarter processing + */ + text = new String("2013 Dec 10 Thu Thu Q4 Q4"); + try { + format.parse(text); + } catch (ParseException pe) { + errln("normal quarter processing failed"); + } + + + + } + } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatTest.java index e04d978a3b0..86bd4e2bd80 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatTest.java @@ -4320,7 +4320,7 @@ public class DateFormatTest extends com.ibm.icu.dev.test.TestFmwk { if(p.getErrorIndex() != -1) continue; else - errln("error: unexpected parse success..."+item.parseString + " w/ lenient="+item.leniency+" should have faile"); + errln("error: unexpected parse success..."+item.parseString + " w/ lenient="+item.leniency+" should have failed"); } if(p.getErrorIndex() != -1) { errln("error: parse error for string " +item.parseString + " -- idx["+p.getIndex()+"] errIdx["+p.getErrorIndex()+"]"); @@ -4335,5 +4335,5 @@ public class DateFormatTest extends com.ibm.icu.dev.test.TestFmwk { } } } - + }