From 2666d18e544271d0f756bf20ee251c2cb699e269 Mon Sep 17 00:00:00 2001 From: Mihai Nita Date: Wed, 12 Dec 2018 10:22:04 -0800 Subject: [PATCH] ICU-9622 Adding support for date/time skeletons --- icu4c/source/common/patternprops.cpp | 10 +++ icu4c/source/common/patternprops.h | 7 +++ icu4c/source/i18n/msgfmt.cpp | 39 +++++++----- icu4c/source/i18n/unicode/msgfmt.h | 5 ++ icu4c/source/test/intltest/tmsgfmt.cpp | 52 +++++++++++++++- icu4c/source/test/intltest/tmsgfmt.h | 5 ++ .../src/com/ibm/icu/text/MessageFormat.java | 24 +++++-- .../dev/test/format/TestMessageFormat.java | 62 +++++++++++++++---- 8 files changed, 166 insertions(+), 38 deletions(-) diff --git a/icu4c/source/common/patternprops.cpp b/icu4c/source/common/patternprops.cpp index 01e33ce109f..c38a7e276de 100644 --- a/icu4c/source/common/patternprops.cpp +++ b/icu4c/source/common/patternprops.cpp @@ -173,6 +173,16 @@ PatternProps::skipWhiteSpace(const UChar *s, int32_t length) { return s; } +int32_t +PatternProps::skipWhiteSpace(const UnicodeString& s, int32_t start) { + int32_t i = start; + int32_t length = s.length(); + while(i= 0) ? DATE_STYLES[styleID] : DateFormat::kDefault; - - if (typeID == 1) { - fmt = DateFormat::createDateInstance(date_style, fLocale); + firstNonSpace = PatternProps::skipWhiteSpace(style, 0); + if (style.compare(firstNonSpace, 2, u"::", 0, 2) == 0) { + // Skeleton + UnicodeString skeleton = style.tempSubString(firstNonSpace + 2); + fmt = DateFormat::createInstanceForSkeleton(skeleton, fLocale, ec); } else { - fmt = DateFormat::createTimeInstance(date_style, fLocale); - } + // Pattern + styleID = findKeyword(style, DATE_STYLE_IDS); + date_style = (styleID >= 0) ? DATE_STYLES[styleID] : DateFormat::kDefault; - if (styleID < 0 && fmt != NULL) { - SimpleDateFormat* sdtfmt = dynamic_cast(fmt); - if (sdtfmt != NULL) { - sdtfmt->applyPattern(style); + if (typeID == 1) { + fmt = DateFormat::createDateInstance(date_style, fLocale); + } else { + fmt = DateFormat::createTimeInstance(date_style, fLocale); + } + + if (styleID < 0 && fmt != NULL) { + SimpleDateFormat* sdtfmt = dynamic_cast(fmt); + if (sdtfmt != NULL) { + sdtfmt->applyPattern(style); + } } } break; diff --git a/icu4c/source/i18n/unicode/msgfmt.h b/icu4c/source/i18n/unicode/msgfmt.h index 074d9335400..a56383517fb 100644 --- a/icu4c/source/i18n/unicode/msgfmt.h +++ b/icu4c/source/i18n/unicode/msgfmt.h @@ -204,6 +204,9 @@ class NumberFormat; * argStyleText * new SimpleDateFormat(argStyleText, getLocale(), status) * + * argSkeletonText + * DateFormat::createInstanceForSkeleton(argSkeletonText, getLocale(), status) + * * time * (none) * DateFormat.createTimeInstance(kDefault, getLocale(), status) @@ -994,6 +997,8 @@ private: void cacheExplicitFormats(UErrorCode& status); + int32_t skipLeadingSpaces(UnicodeString& style); + Format* createAppropriateFormat(UnicodeString& type, UnicodeString& style, Formattable::Type& formattableType, diff --git a/icu4c/source/test/intltest/tmsgfmt.cpp b/icu4c/source/test/intltest/tmsgfmt.cpp index 7249001f5eb..89f3222fd90 100644 --- a/icu4c/source/test/intltest/tmsgfmt.cpp +++ b/icu4c/source/test/intltest/tmsgfmt.cpp @@ -21,6 +21,7 @@ #include "tmsgfmt.h" #include "cmemory.h" +#include "loctest.h" #include "unicode/format.h" #include "unicode/decimfmt.h" @@ -72,6 +73,8 @@ TestMessageFormat::runIndexedTest(int32_t index, UBool exec, TESTCASE_AUTO(TestDecimals); TESTCASE_AUTO(TestArgIsPrefixOfAnother); TESTCASE_AUTO(TestMessageFormatNumberSkeleton); + TESTCASE_AUTO(TestMessageFormatDateSkeleton); + TESTCASE_AUTO(TestMessageFormatTimeSkeleton); TESTCASE_AUTO_END; } @@ -1085,7 +1088,7 @@ void TestMessageFormat::testFormat() result = msg.format( *fmt, result, - //FieldPosition(0), + //FieldPosition(FieldPosition::DONT_CARE), fp, err); @@ -1099,7 +1102,7 @@ void TestMessageFormat::testFormat() result = msg.format( ft_arr, result, - //FieldPosition(0), + //FieldPosition(FieldPosition::DONT_CARE), fp, err); @@ -2020,7 +2023,7 @@ void TestMessageFormat::TestMessageFormatNumberSkeleton() { status.setScope(cas.messagePattern); MessageFormat msgf(cas.messagePattern, cas.localeName, status); UnicodeString sb; - FieldPosition fpos(0); + FieldPosition fpos(FieldPosition::DONT_CARE); Formattable argsArray[] = {{cas.arg}}; Formattable args(argsArray, 1); msgf.format(args, sb, status); @@ -2029,4 +2032,47 @@ void TestMessageFormat::TestMessageFormatNumberSkeleton() { } } +void TestMessageFormat::doTheRealDateTimeSkeletonTesting(UDate testDate, + const char16_t* messagePattern, const char* localeName, const char16_t* expected, + IcuTestErrorCode& status) { + + status.setScope(messagePattern); + MessageFormat msgf(messagePattern, localeName, status); + UnicodeString sb; + FieldPosition fpos(FieldPosition::DONT_CARE); + Formattable argsArray[] = { Formattable(testDate, Formattable::kIsDate) }; + Formattable args(argsArray, 1); + msgf.format(args, sb, status); + + assertEquals(messagePattern, expected, sb); +} + +void TestMessageFormat::TestMessageFormatDateSkeleton() { + IcuTestErrorCode status(*this, "TestMessageFormatDateSkeleton"); + + UDate date = LocaleTest::date(2021-1900, UCAL_NOVEMBER, 23, 16, 42, 55); + + doTheRealDateTimeSkeletonTesting(date, u"{0,date,::MMMMd}", "en", u"November 23", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,date,::yMMMMdjm}", "en", u"November 23, 2021, 4:42 PM", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,date, :: yMMMMd }", "en", u"November 23, 2021", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,date,::yMMMMd}", "fr", u"23 novembre 2021", status); + doTheRealDateTimeSkeletonTesting(date, u"Expiration: {0,date,::yMMM}!", "en", u"Expiration: Nov 2021!", status); + // pattern literal + doTheRealDateTimeSkeletonTesting(date, u"{0,date,'::'yMMMMd}", "en", u"::2021November23", status); +} + +void TestMessageFormat::TestMessageFormatTimeSkeleton() { + IcuTestErrorCode status(*this, "TestMessageFormatTimeSkeleton"); + + UDate date = LocaleTest::date(2021-1900, UCAL_NOVEMBER, 23, 16, 42, 55); + + doTheRealDateTimeSkeletonTesting(date, u"{0,time,::MMMMd}", "en", u"November 23", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,time,::yMMMMdjm}", "en", u"November 23, 2021, 4:42 PM", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,time, :: yMMMMd }", "en", u"November 23, 2021", status); + doTheRealDateTimeSkeletonTesting(date, u"{0,time,::yMMMMd}", "fr", u"23 novembre 2021", status); + doTheRealDateTimeSkeletonTesting(date, u"Expiration: {0,time,::yMMM}!", "en", u"Expiration: Nov 2021!", status); + // pattern literal + doTheRealDateTimeSkeletonTesting(date, u"{0,time,'::'yMMMMd}", "en", u"::2021November23", status); +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/tmsgfmt.h b/icu4c/source/test/intltest/tmsgfmt.h index d4bc13d9eaa..dd2153650ab 100644 --- a/icu4c/source/test/intltest/tmsgfmt.h +++ b/icu4c/source/test/intltest/tmsgfmt.h @@ -122,9 +122,14 @@ public: void TestDecimals(); void TestArgIsPrefixOfAnother(); void TestMessageFormatNumberSkeleton(); + void TestMessageFormatDateSkeleton(); + void TestMessageFormatTimeSkeleton(); private: UnicodeString GetPatternAndSkipSyntax(const MessagePattern& pattern); + void doTheRealDateTimeSkeletonTesting(UDate testDate, + const char16_t* messagePattern, const char* localeName, const char16_t* expected, + IcuTestErrorCode& status); }; #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java index 4da8defe53f..9767dd6bd9b 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java @@ -202,6 +202,9 @@ import com.ibm.icu.util.ULocale.Category; * argStyleText * new SimpleDateFormat(argStyleText, getLocale()) * + * argSkeletonText + * DateFormat.getInstanceForSkeleton(argSkeletonText, getLocale()) + * * time * (none) * DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale()) @@ -2188,6 +2191,16 @@ public class MessageFormat extends UFormat { DATE_MODIFIER_LONG = 3, DATE_MODIFIER_FULL = 4; + Format dateTimeFormatForPatternOrSkeleton(String style) { + // Ignore leading whitespace when looking for "::", the skeleton signal sequence + int i = PatternProps.skipWhiteSpace(style, 0); + if (style.regionMatches(i, "::", 0, 2)) { // Skeleton + return DateFormat.getInstanceForSkeleton(style.substring(i + 2), ulocale); + } else { // Pattern + return new SimpleDateFormat(style, ulocale); + } + } + // Creates an appropriate Format object for the type and style passed. // Both arguments cannot be null. private Format createAppropriateFormat(String type, String style) { @@ -2210,8 +2223,7 @@ public class MessageFormat extends UFormat { break; default: // pattern or skeleton // Ignore leading whitespace when looking for "::", the skeleton signal sequence - int i = 0; - for (; PatternProps.isWhiteSpace(style.charAt(i)); i++); + int i = PatternProps.skipWhiteSpace(style, 0); if (style.regionMatches(i, "::", 0, 2)) { // Skeleton newFormat = NumberFormatter.forSkeleton(style.substring(i + 2)).locale(ulocale).toFormat(); @@ -2239,8 +2251,8 @@ public class MessageFormat extends UFormat { case DATE_MODIFIER_FULL: newFormat = DateFormat.getDateInstance(DateFormat.FULL, ulocale); break; - default: - newFormat = new SimpleDateFormat(style, ulocale); + default: // pattern or skeleton + newFormat = dateTimeFormatForPatternOrSkeleton(style); break; } break; @@ -2261,8 +2273,8 @@ public class MessageFormat extends UFormat { case DATE_MODIFIER_FULL: newFormat = DateFormat.getTimeInstance(DateFormat.FULL, ulocale); break; - default: - newFormat = new SimpleDateFormat(style, ulocale); + default: // pattern or skeleton + newFormat = dateTimeFormatForPatternOrSkeleton(style); break; } break; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java index 39235e96b3d..10d65002ebf 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java @@ -40,6 +40,8 @@ import com.ibm.icu.text.MessagePattern; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.SimpleDateFormat; import com.ibm.icu.text.UFormat; +import com.ibm.icu.util.Calendar; +import com.ibm.icu.util.GregorianCalendar; import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.ULocale; @@ -111,7 +113,7 @@ public class TestMessageFormat extends TestFmwk { errln("Number format creation failed for " + locale[i].getDisplayName()); continue; } - FieldPosition pos = new FieldPosition(0); + FieldPosition pos = new FieldPosition(FieldPosition_DONT_CARE); buffer.setLength(0); form.format(myNumber, buffer, pos); parsePos.setIndex(0); @@ -215,7 +217,7 @@ public class TestMessageFormat extends TestFmwk { //it_out << "Pat out: " << form.toPattern(buffer)); StringBuffer result = new StringBuffer(); - FieldPosition fieldpos = new FieldPosition(0); + FieldPosition fieldpos = new FieldPosition(FieldPosition_DONT_CARE); form.format(testArgs, result, fieldpos); assertEquals("format", testResultStrings[i], result.toString()); @@ -260,7 +262,7 @@ public class TestMessageFormat extends TestFmwk { return; } Object testArgs1[] = { "abc", "def" }; - FieldPosition fieldpos = new FieldPosition(0); + FieldPosition fieldpos = new FieldPosition(FieldPosition_DONT_CARE); assertEquals("format", "There are abc files on def", form.format(testArgs1, buffer2, fieldpos).toString()); @@ -458,7 +460,7 @@ public class TestMessageFormat extends TestFmwk { MessageFormat msg = new MessageFormat(formatStr, Locale.ENGLISH); result.setLength(0); - FieldPosition pos = new FieldPosition(0); + FieldPosition pos = new FieldPosition(FieldPosition_DONT_CARE); result = msg.format( arguments, result, @@ -512,7 +514,7 @@ public class TestMessageFormat extends TestFmwk { String compareStr = "On Aug 8, 1997, it began."; MessageFormat msg = new MessageFormat(formatStr); - FieldPosition fp = new FieldPosition(0); + FieldPosition fp = new FieldPosition(FieldPosition_DONT_CARE); try { msg.format(new Date(871068000000L), @@ -923,7 +925,7 @@ public class TestMessageFormat extends TestFmwk { MessageFormat msg = new MessageFormat(formatStr, ULocale.US); result.setLength(0); - FieldPosition pos = new FieldPosition(0); + FieldPosition pos = new FieldPosition(FieldPosition_DONT_CARE); result = msg.format( arguments, result, @@ -940,7 +942,7 @@ public class TestMessageFormat extends TestFmwk { msg.setFormatsByArgumentIndex(fmts); result.setLength(0); - pos = new FieldPosition(0); + pos = new FieldPosition(FieldPosition_DONT_CARE); result = msg.format( arguments, result, @@ -952,7 +954,7 @@ public class TestMessageFormat extends TestFmwk { Format newFmt = NumberFormat.getCurrencyInstance(ULocale.GERMAN); msg.setFormatByArgumentIndex(0, newFmt); result.setLength(0); - pos = new FieldPosition(0); + pos = new FieldPosition(FieldPosition_DONT_CARE); result = msg.format( arguments, result, @@ -1008,7 +1010,7 @@ public class TestMessageFormat extends TestFmwk { String compareStr = "On Aug 8, 1997, it began."; MessageFormat msg = new MessageFormat(formatStr); - FieldPosition fp = new FieldPosition(0); + FieldPosition fp = new FieldPosition(FieldPosition_DONT_CARE); try { msg.format(arguments.get("startDate"), result, fp); @@ -1118,7 +1120,7 @@ public class TestMessageFormat extends TestFmwk { gotException = false; try { Object args[] = {new Long(42)}; - msg.format(args, new StringBuffer(), new FieldPosition(0)); + msg.format(args, new StringBuffer(), new FieldPosition(FieldPosition_DONT_CARE)); } catch (IllegalArgumentException e) { gotException = true; } @@ -1131,7 +1133,7 @@ public class TestMessageFormat extends TestFmwk { gotException = false; try { Object args[] = {new Long(42)}; - msg.format((Object) args, new StringBuffer(), new FieldPosition(0)); + msg.format((Object) args, new StringBuffer(), new FieldPosition(FieldPosition_DONT_CARE)); } catch (IllegalArgumentException e) { gotException = true; } @@ -1878,7 +1880,7 @@ public class TestMessageFormat extends TestFmwk { map.put("_oOo_", new Integer(3)); StringBuffer result = new StringBuffer(); assertEquals("trim-named-arg format() failed", "x 3 y", - m.format(map, result, new FieldPosition(0)).toString()); + m.format(map, result, new FieldPosition(FieldPosition_DONT_CARE)).toString()); } @Test @@ -2116,10 +2118,44 @@ public class TestMessageFormat extends TestFmwk { MessageFormat msgf = new MessageFormat(messagePattern, locale); StringBuffer sb = new StringBuffer(); - FieldPosition fpos = new FieldPosition(0); + FieldPosition fpos = new FieldPosition(FieldPosition_DONT_CARE); msgf.format(new Object[] { arg }, sb, fpos); assertEquals(messagePattern, expected, sb.toString()); } } + + private static void doTheRealDateTimeSkeletonTesting(Date date, String messagePattern, ULocale locale, String expected) { + + MessageFormat msgf = new MessageFormat(messagePattern, locale); + StringBuffer sb = new StringBuffer(); + FieldPosition fpos = new FieldPosition(FieldPosition_DONT_CARE); + msgf.format(new Object[] { date }, sb, fpos); + + assertEquals(messagePattern, expected, sb.toString()); + } + + @Test + public void TestMessageFormatDateSkeleton() { + Date date = new GregorianCalendar(2021, Calendar.NOVEMBER, 23, 16, 42, 55).getTime(); + + doTheRealDateTimeSkeletonTesting(date, "{0,date,::MMMMd}", ULocale.ENGLISH, "November 23"); + doTheRealDateTimeSkeletonTesting(date, "{0,date,::yMMMMdjm}", ULocale.ENGLISH, "November 23, 2021, 4:42 PM"); + doTheRealDateTimeSkeletonTesting(date, "{0,date, :: yMMMMd }", ULocale.ENGLISH, "November 23, 2021"); + doTheRealDateTimeSkeletonTesting(date, "{0,date,::yMMMMd}", ULocale.FRENCH, "23 novembre 2021"); + doTheRealDateTimeSkeletonTesting(date, "Expiration: {0,date,::yMMM}!", ULocale.ENGLISH, "Expiration: Nov 2021!"); + doTheRealDateTimeSkeletonTesting(date, "{0,date,'::'yMMMMd}", ULocale.ENGLISH, "::2021November23"); // pattern literal + } + + @Test + public void TestMessageFormatTimeSkeleton() { + Date date = new GregorianCalendar(2021, Calendar.NOVEMBER, 23, 16, 42, 55).getTime(); + + doTheRealDateTimeSkeletonTesting(date, "{0,time,::MMMMd}", ULocale.ENGLISH, "November 23"); + doTheRealDateTimeSkeletonTesting(date, "{0,time,::yMMMMdjm}", ULocale.ENGLISH, "November 23, 2021, 4:42 PM"); + doTheRealDateTimeSkeletonTesting(date, "{0,time, :: yMMMMd }", ULocale.ENGLISH, "November 23, 2021"); + doTheRealDateTimeSkeletonTesting(date, "{0,time,::yMMMMd}", ULocale.FRENCH, "23 novembre 2021"); + doTheRealDateTimeSkeletonTesting(date, "Expiration: {0,time,::yMMM}!", ULocale.ENGLISH, "Expiration: Nov 2021!"); + doTheRealDateTimeSkeletonTesting(date, "{0,time,'::'yMMMMd}", ULocale.ENGLISH, "::2021November23"); // pattern literal + } }