]> granicus.if.org Git - icu/commitdiff
ICU-9622 Adding support for date/time skeletons
authorMihai Nita <mnita@google.com>
Wed, 12 Dec 2018 18:22:04 +0000 (10:22 -0800)
committerMarkus Scherer <markus.icu@gmail.com>
Thu, 13 Dec 2018 21:20:33 +0000 (13:20 -0800)
icu4c/source/common/patternprops.cpp
icu4c/source/common/patternprops.h
icu4c/source/i18n/msgfmt.cpp
icu4c/source/i18n/unicode/msgfmt.h
icu4c/source/test/intltest/tmsgfmt.cpp
icu4c/source/test/intltest/tmsgfmt.h
icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java

index 01e33ce109f57aa593567c5c6f3991a888abc21e..c38a7e276def15b815666193709d08f2eeddca32 100644 (file)
@@ -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<length && isWhiteSpace(s.charAt(i))) {
+        ++i;
+    }
+    return i;
+}
+
 const UChar *
 PatternProps::trimWhiteSpace(const UChar *s, int32_t &length) {
     if(length<=0 || (!isWhiteSpace(s[0]) && !isWhiteSpace(s[length-1]))) {
index a42eb3c244128a2c560bab1551b8aac242ed6f25..b57cdeb6e534f624de7d7ed0237524fa3458771b 100644 (file)
@@ -17,6 +17,7 @@
 #ifndef __PATTERNPROPS_H__
 #define __PATTERNPROPS_H__
 
+#include "unicode/unistr.h"
 #include "unicode/utypes.h"
 
 U_NAMESPACE_BEGIN
@@ -63,6 +64,12 @@ public:
      */
     static const UChar *skipWhiteSpace(const UChar *s, int32_t length);
 
+    /**
+     * Skips over Pattern_White_Space starting at index start in s.
+     * @return The smallest index at or after start with a non-white space character.
+     */
+    static int32_t skipWhiteSpace(const UnicodeString &s, int32_t start);
+
     /**
      * @return s except with leading and trailing Pattern_White_Space removed and length adjusted.
      */
index 8ff86a2cacf018116a8683ca264b4de9b2a76899..1b1f6708d29a503b472101ec91bf41ac181bc25e 100644 (file)
@@ -1673,7 +1673,6 @@ void MessageFormat::cacheExplicitFormats(UErrorCode& status) {
     }
 }
 
-
 Format* MessageFormat::createAppropriateFormat(UnicodeString& type, UnicodeString& style,
                                                Formattable::Type& formattableType, UParseError& parseError,
                                                UErrorCode& ec) {
@@ -1683,6 +1682,7 @@ Format* MessageFormat::createAppropriateFormat(UnicodeString& type, UnicodeStrin
     Format* fmt = NULL;
     int32_t typeID, styleID;
     DateFormat::EStyle date_style;
+    int32_t firstNonSpace;
 
     switch (typeID = findKeyword(type, TYPE_IDS)) {
     case 0: // number
@@ -1702,11 +1702,10 @@ Format* MessageFormat::createAppropriateFormat(UnicodeString& type, UnicodeStrin
             fmt = createIntegerFormat(fLocale, ec);
             break;
         default: // pattern or skeleton
-            int32_t i = 0;
-            for (; PatternProps::isWhiteSpace(style.charAt(i)); i++);
-            if (style.compare(i, 2, u"::", 0, 2) == 0) {
+            firstNonSpace = PatternProps::skipWhiteSpace(style, 0);
+            if (style.compare(firstNonSpace, 2, u"::", 0, 2) == 0) {
                 // Skeleton
-                UnicodeString skeleton = style.tempSubString(i + 2);
+                UnicodeString skeleton = style.tempSubString(firstNonSpace + 2);
                 fmt = number::NumberFormatter::forSkeleton(skeleton, ec).locale(fLocale).toFormat(ec);
             } else {
                 // Pattern
@@ -1725,19 +1724,27 @@ Format* MessageFormat::createAppropriateFormat(UnicodeString& type, UnicodeStrin
     case 1: // date
     case 2: // time
         formattableType = Formattable::kDate;
-        styleID = findKeyword(style, DATE_STYLE_IDS);
-        date_style = (styleID >= 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<SimpleDateFormat*>(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<SimpleDateFormat*>(fmt);
+                if (sdtfmt != NULL) {
+                    sdtfmt->applyPattern(style);
+                }
             }
         }
         break;
index 074d93354000ef28d1e69ba5cb928cf770b9618d..a56383517fb8ec63c4bd9ff7ddaf3cfd686b4c99 100644 (file)
@@ -204,6 +204,9 @@ class NumberFormat;
  *       <td><i>argStyleText</i>
  *       <td><code>new SimpleDateFormat(argStyleText, getLocale(), status)</code>
  *    <tr>
+ *       <td><i>argSkeletonText</i>
+ *       <td><code>DateFormat::createInstanceForSkeleton(argSkeletonText, getLocale(), status)</code>
+ *    <tr>
  *       <td rowspan=6><code>time</code>
  *       <td><i>(none)</i>
  *       <td><code>DateFormat.createTimeInstance(kDefault, getLocale(), status)</code>
@@ -994,6 +997,8 @@ private:
 
     void cacheExplicitFormats(UErrorCode& status);
 
+    int32_t skipLeadingSpaces(UnicodeString& style);
+
     Format* createAppropriateFormat(UnicodeString& type,
                                     UnicodeString& style,
                                     Formattable::Type& formattableType,
index 7249001f5eb21bd4132c6e643d5279a6568ddef2..89f3222fd9066d5918ba11d739890accdd89fa8a 100644 (file)
@@ -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 */
index d4bc13d9eaad855c26d48c94baec5e5e93ea8f5e..dd2153650abf74e72efa69d912ea8f712823f932 100644 (file)
@@ -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 */
index 4da8defe53f89fa14e64ffed695fe885d46ee7e1..9767dd6bd9b7800af7d2335b1b3ad70f341d70f3 100644 (file)
@@ -202,6 +202,9 @@ import com.ibm.icu.util.ULocale.Category;
  *       <td><i>argStyleText</i>
  *       <td><code>new SimpleDateFormat(argStyleText, getLocale())</code>
  *    <tr>
+ *       <td><i>argSkeletonText</i>
+ *       <td><code>DateFormat.getInstanceForSkeleton(argSkeletonText, getLocale())</code>
+ *    <tr>
  *       <td rowspan=6><code>time</code>
  *       <td><i>(none)</i>
  *       <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
@@ -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;
index 39235e96b3dccc21149516e2e0b3fdb3fd6fb16e..10d65002ebf8906c495f2776330382579a7df6f1 100644 (file)
@@ -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
+    }
 }