]> granicus.if.org Git - icu/commitdiff
ICU-13742 Implementing number skeletons in MessageFormat.
authorShane Carr <shane@unicode.org>
Tue, 15 May 2018 00:05:04 +0000 (00:05 +0000)
committerShane Carr <shane@unicode.org>
Tue, 15 May 2018 00:05:04 +0000 (00:05 +0000)
X-SVN-Rev: 41377

16 files changed:
icu4c/source/i18n/msgfmt.cpp
icu4c/source/i18n/number_fluent.cpp
icu4c/source/i18n/number_utils.cpp
icu4c/source/i18n/number_utils.h
icu4c/source/i18n/unicode/numberformatter.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4c/source/test/intltest/tmsgfmt.cpp
icu4c/source/test/intltest/tmsgfmt.h
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LocalizedNumberFormatterAsFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessageRegressionTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java

index faf6c21c6eb4b5802558ecb41fe9467ac2227ceb..0287da699cbb828e06e8fd67b9699b92f06a5608 100644 (file)
@@ -31,6 +31,7 @@
 #include "unicode/decimfmt.h"
 #include "unicode/localpointer.h"
 #include "unicode/msgfmt.h"
+#include "unicode/numberformatter.h"
 #include "unicode/plurfmt.h"
 #include "unicode/rbnf.h"
 #include "unicode/selfmt.h"
@@ -1700,12 +1701,21 @@ Format* MessageFormat::createAppropriateFormat(UnicodeString& type, UnicodeStrin
             formattableType = Formattable::kLong;
             fmt = createIntegerFormat(fLocale, ec);
             break;
-        default: // pattern
-            fmt = NumberFormat::createInstance(fLocale, ec);
-            if (fmt) {
-                DecimalFormat* decfmt = dynamic_cast<DecimalFormat*>(fmt);
-                if (decfmt != NULL) {
-                    decfmt->applyPattern(style,parseError,ec);
+        default: // pattern or skeleton
+            int32_t i = 0;
+            for (; PatternProps::isWhiteSpace(style.charAt(i)); i++);
+            if (style.compare(i, 2, u"::", 0, 2) == 0) {
+                // Skeleton
+                UnicodeString skeleton = style.tempSubString(i + 2);
+                fmt = number::NumberFormatter::fromSkeleton(skeleton, ec).locale(fLocale).toFormat(ec);
+            } else {
+                // Pattern
+                fmt = NumberFormat::createInstance(fLocale, ec);
+                if (fmt) {
+                    auto* decfmt = dynamic_cast<DecimalFormat*>(fmt);
+                    if (decfmt != nullptr) {
+                        decfmt->applyPattern(style, parseError, ec);
+                    }
                 }
             }
             break;
index 0d3ee2d1238c2c9eb72a8e38a64d987791f2012c..a1915fdcbc0caa7a037c591da60aaace5259e956 100644 (file)
@@ -11,6 +11,7 @@
 #include "number_formatimpl.h"
 #include "umutex.h"
 #include "number_skeletons.h"
+#include "number_utils.h"
 #include "number_utypes.h"
 #include "util.h"
 
@@ -575,23 +576,6 @@ const NumberingSystem* SymbolsWrapper::getNumberingSystem() const {
 }
 
 
-FormattedNumber::FormattedNumber(FormattedNumber&& src) U_NOEXCEPT
-        : fResults(src.fResults), fErrorCode(src.fErrorCode) {
-    // Disown src.fResults to prevent double-deletion
-    src.fResults = nullptr;
-    src.fErrorCode = U_INVALID_STATE_ERROR;
-}
-
-FormattedNumber& FormattedNumber::operator=(FormattedNumber&& src) U_NOEXCEPT {
-    delete fResults;
-    fResults = src.fResults;
-    fErrorCode = src.fErrorCode;
-    // Disown src.fResults to prevent double-deletion
-    src.fResults = nullptr;
-    src.fErrorCode = U_INVALID_STATE_ERROR;
-    return *this;
-}
-
 FormattedNumber LocalizedNumberFormatter::formatInt(int64_t value, UErrorCode& status) const {
     if (U_FAILURE(status)) { return FormattedNumber(U_ILLEGAL_ARGUMENT_ERROR); }
     auto results = new UFormattedNumberData();
@@ -745,6 +729,30 @@ int32_t LocalizedNumberFormatter::getCallCount() const {
     return umtx_loadAcquire(*callCount);
 }
 
+Format* LocalizedNumberFormatter::toFormat(UErrorCode& status) const {
+    LocalPointer<LocalizedNumberFormatterAsFormat> retval(
+            new LocalizedNumberFormatterAsFormat(*this, fMacros.locale), status);
+    return retval.orphan();
+}
+
+
+FormattedNumber::FormattedNumber(FormattedNumber&& src) U_NOEXCEPT
+        : fResults(src.fResults), fErrorCode(src.fErrorCode) {
+    // Disown src.fResults to prevent double-deletion
+    src.fResults = nullptr;
+    src.fErrorCode = U_INVALID_STATE_ERROR;
+}
+
+FormattedNumber& FormattedNumber::operator=(FormattedNumber&& src) U_NOEXCEPT {
+    delete fResults;
+    fResults = src.fResults;
+    fErrorCode = src.fErrorCode;
+    // Disown src.fResults to prevent double-deletion
+    src.fResults = nullptr;
+    src.fErrorCode = U_INVALID_STATE_ERROR;
+    return *this;
+}
+
 UnicodeString FormattedNumber::toString() const {
     UErrorCode localStatus = U_ZERO_ERROR;
     return toString(localStatus);
index 2a74dba5ab17aaf934f3c0b97eeeb4b477b1f181..da653e4a6d993f023955792682aa0e9507d98729 100644 (file)
@@ -19,6 +19,7 @@
 #include "double-conversion.h"
 #include "uresimp.h"
 #include "ureslocs.h"
+#include "number_utypes.h"
 
 using namespace icu;
 using namespace icu::number;
@@ -47,6 +48,85 @@ doGetPattern(UResourceBundle* res, const char* nsName, const char* patternKey, U
 }
 
 
+LocalizedNumberFormatterAsFormat::LocalizedNumberFormatterAsFormat(
+        const LocalizedNumberFormatter& formatter, const Locale& locale)
+        : fFormatter(formatter), fLocale(locale) {
+    const char* localeName = locale.getName();
+    setLocaleIDs(localeName, localeName);
+}
+
+LocalizedNumberFormatterAsFormat::~LocalizedNumberFormatterAsFormat() = default;
+
+UBool LocalizedNumberFormatterAsFormat::operator==(const Format& other) const {
+    auto* _other = dynamic_cast<const LocalizedNumberFormatterAsFormat*>(&other);
+    if (_other == nullptr) {
+        return false;
+    }
+    // TODO: Change this to use LocalizedNumberFormatter::operator== if it is ever proposed.
+    // This implementation is fine, but not particularly efficient.
+    UErrorCode localStatus = U_ZERO_ERROR;
+    return fFormatter.toSkeleton(localStatus) == _other->fFormatter.toSkeleton(localStatus);
+}
+
+Format* LocalizedNumberFormatterAsFormat::clone() const {
+    return new LocalizedNumberFormatterAsFormat(*this);
+}
+
+UnicodeString& LocalizedNumberFormatterAsFormat::format(const Formattable& obj, UnicodeString& appendTo,
+                                                        FieldPosition& pos, UErrorCode& status) const {
+    if (U_FAILURE(status)) { return appendTo; }
+    UFormattedNumberData data;
+    obj.populateDecimalQuantity(data.quantity, status);
+    if (U_FAILURE(status)) {
+        return appendTo;
+    }
+    fFormatter.formatImpl(&data, status);
+    if (U_FAILURE(status)) {
+        return appendTo;
+    }
+    // always return first occurrence:
+    pos.setBeginIndex(0);
+    pos.setEndIndex(0);
+    bool found = data.string.nextFieldPosition(pos, status);
+    if (found && appendTo.length() != 0) {
+        pos.setBeginIndex(pos.getBeginIndex() + appendTo.length());
+        pos.setEndIndex(pos.getEndIndex() + appendTo.length());
+    }
+    appendTo.append(data.string.toTempUnicodeString());
+    return appendTo;
+}
+
+UnicodeString& LocalizedNumberFormatterAsFormat::format(const Formattable& obj, UnicodeString& appendTo,
+                                                        FieldPositionIterator* posIter,
+                                                        UErrorCode& status) const {
+    if (U_FAILURE(status)) { return appendTo; }
+    UFormattedNumberData data;
+    obj.populateDecimalQuantity(data.quantity, status);
+    if (U_FAILURE(status)) {
+        return appendTo;
+    }
+    fFormatter.formatImpl(&data, status);
+    if (U_FAILURE(status)) {
+        return appendTo;
+    }
+    appendTo.append(data.string.toTempUnicodeString());
+    if (posIter != nullptr) {
+        data.string.getAllFieldPositions(*posIter, status);
+    }
+    return appendTo;
+}
+
+void LocalizedNumberFormatterAsFormat::parseObject(const UnicodeString&, Formattable&,
+                                                   ParsePosition& parse_pos) const {
+    // Not supported.
+    parse_pos.setErrorIndex(0);
+}
+
+const LocalizedNumberFormatter& LocalizedNumberFormatterAsFormat::getNumberFormatter() const {
+    return fFormatter;
+}
+
+
 const char16_t* utils::getPatternForStyle(const Locale& locale, const char* nsName, CldrPatternStyle style,
                                           UErrorCode& status) {
     const char* patternKey;
index df6f0d3080fd050ccefc9f5c1eb0e6029beb866b..86f56983a402747c58add1ed0964556a4b626cf9 100644 (file)
@@ -72,6 +72,82 @@ struct MicroProps : public MicroPropsGenerator {
     bool exhausted = false;
 };
 
+
+/**
+ * A wrapper around LocalizedNumberFormatter implementing the Format interface, enabling improved
+ * compatibility with other APIs.
+ *
+ * @draft ICU 62
+ * @see NumberFormatter
+ */
+class U_I18N_API LocalizedNumberFormatterAsFormat : public Format {
+  public:
+    LocalizedNumberFormatterAsFormat(const LocalizedNumberFormatter& formatter, const Locale& locale);
+
+    /**
+     * Destructor.
+     */
+    ~LocalizedNumberFormatterAsFormat() U_OVERRIDE;
+
+    /**
+     * Equals operator.
+     */
+    UBool operator==(const Format& other) const U_OVERRIDE;
+
+    /**
+     * Creates a copy of this object.
+     */
+    Format* clone() const U_OVERRIDE;
+
+    /**
+     * Formats a Number using the wrapped LocalizedNumberFormatter. The provided formattable must be a
+     * number type.
+     */
+    UnicodeString& format(const Formattable& obj, UnicodeString& appendTo, FieldPosition& pos,
+                          UErrorCode& status) const U_OVERRIDE;
+
+    /**
+     * Formats a Number using the wrapped LocalizedNumberFormatter. The provided formattable must be a
+     * number type.
+     */
+    UnicodeString& format(const Formattable& obj, UnicodeString& appendTo, FieldPositionIterator* posIter,
+                          UErrorCode& status) const U_OVERRIDE;
+
+    /**
+     * Not supported: sets an error index and returns.
+     */
+    void parseObject(const UnicodeString& source, Formattable& result,
+                     ParsePosition& parse_pos) const U_OVERRIDE;
+
+    /**
+     * Gets the LocalizedNumberFormatter that this wrapper class uses to format numbers.
+     *
+     * For maximum efficiency, this function returns by const reference. You must copy the return value
+     * into a local variable if you want to use it beyond the lifetime of the current object:
+     *
+     * <pre>
+     * LocalizedNumberFormatter localFormatter = fmt->getNumberFormatter();
+     * </pre>
+     *
+     * You can however use the return value directly when chaining:
+     *
+     * <pre>
+     * FormattedNumber result = fmt->getNumberFormatter().formatDouble(514.23, status);
+     * </pre>
+     *
+     * @return The unwrapped LocalizedNumberFormatter.
+     */
+    const LocalizedNumberFormatter& getNumberFormatter() const;
+
+  private:
+    LocalizedNumberFormatter fFormatter;
+
+    // Even though the locale is inside the LocalizedNumberFormatter, we have to keep it here, too, because
+    // LocalizedNumberFormatter doesn't have a getLocale() method, and ICU-TC didn't want to add one.
+    Locale fLocale;
+};
+
+
 enum CldrPatternStyle {
     CLDR_PATTERN_STYLE_DECIMAL,
     CLDR_PATTERN_STYLE_CURRENCY,
index e44259c6e8c6c36ec7fce516096445cc769cd16a..3fc77d83f112511a622ded9ccdce5010ba41dca9 100644 (file)
@@ -2265,6 +2265,21 @@ class U_I18N_API LocalizedNumberFormatter
 
 #endif
 
+    /**
+     * Creates a representation of this LocalizedNumberFormat as an icu::Format, enabling the use
+     * of this number formatter with APIs that need an object of that type, such as MessageFormat.
+     *
+     * This API is not intended to be used other than for enabling API compatibility. The formatDouble,
+     * formatInt, and formatDecimal methods should normally be used when formatting numbers, not the Format
+     * object returned by this method.
+     *
+     * The caller owns the returned object and must delete it when finished.
+     *
+     * @return A Format wrapping this LocalizedNumberFormatter.
+     * @draft ICU 62
+     */
+    Format* toFormat(UErrorCode& status) const;
+
     /**
      * Default constructor: puts the formatter into a valid but undefined state.
      *
index e689196088096bf4d4a0b89bad58ebea0cd0bcef..69c3ee44edcef9eecd54542aaae6792e66a2b04e 100644 (file)
@@ -68,6 +68,7 @@ class NumberFormatterApiTest : public IntlTest {
     void locale();
     void formatTypes();
     void fieldPosition();
+    void toFormat();
     void errors();
     void validRanges();
     void copyMove();
index b8ddd44b9f383c08d4cf121cd619bdfae52bdcd0..e74d4958d4d7d13acbf7ec956140a0ca641b0c68 100644 (file)
@@ -11,6 +11,7 @@
 #include "unicode/unum.h"
 #include "unicode/numberformatter.h"
 #include "number_types.h"
+#include "number_utils.h"
 #include "numbertest.h"
 #include "unicode/utypes.h"
 
@@ -82,6 +83,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
         TESTCASE_AUTO(locale);
         TESTCASE_AUTO(formatTypes);
         TESTCASE_AUTO(fieldPosition);
+        TESTCASE_AUTO(toFormat);
         TESTCASE_AUTO(errors);
         TESTCASE_AUTO(validRanges);
         TESTCASE_AUTO(copyMove);
@@ -2155,6 +2157,30 @@ void NumberFormatterApiTest::fieldPosition() {
     assertFalse(u"No fraction part in an integer", fmtd.nextFieldPosition(actual, status));
 }
 
+void NumberFormatterApiTest::toFormat() {
+    IcuTestErrorCode status(*this, "icuFormat");
+    LocalizedNumberFormatter lnf = NumberFormatter::withLocale("fr")
+            .precision(Precision::fixedFraction(3));
+    LocalPointer<Format> format(lnf.toFormat(status), status);
+    FieldPosition fpos(UNUM_DECIMAL_SEPARATOR_FIELD);
+    UnicodeString sb;
+    format->format(514.23, sb, fpos, status);
+    assertEquals("Should correctly format number", u"514,230", sb);
+    assertEquals("Should find decimal separator", 3, fpos.getBeginIndex());
+    assertEquals("Should find end of decimal separator", 4, fpos.getEndIndex());
+    assertEquals(
+            "ICU Format should round-trip",
+            lnf.toSkeleton(status),
+            dynamic_cast<LocalizedNumberFormatterAsFormat*>(format.getAlias())->getNumberFormatter()
+                    .toSkeleton(status));
+
+    FieldPositionIterator fpi1;
+    lnf.formatDouble(514.23, status).getAllFieldPositions(fpi1, status);
+    FieldPositionIterator fpi2;
+    format->format(514.23, sb.remove(), &fpi2, status);
+    assertTrue("Should produce same field position iterator", fpi1 == fpi2);
+}
+
 void NumberFormatterApiTest::errors() {
     LocalizedNumberFormatter lnf = NumberFormatter::withLocale(Locale::getEnglish()).precision(
             Precision::fixedFraction(
index 72557fe8d6fbd0555798b3e792ccf1965c98d863..3a16c285ec41ab3af0364c9474c0c9806e75d5f7 100644 (file)
@@ -71,6 +71,7 @@ TestMessageFormat::runIndexedTest(int32_t index, UBool exec,
     TESTCASE_AUTO(TestSelectOrdinal);
     TESTCASE_AUTO(TestDecimals);
     TESTCASE_AUTO(TestArgIsPrefixOfAnother);
+    TESTCASE_AUTO(TestMessageFormatNumberSkeleton);
     TESTCASE_AUTO_END;
 }
 
@@ -1992,4 +1993,38 @@ void TestMessageFormat::TestArgIsPrefixOfAnother() {
     assertEquals("aa aaa", "AB ABC", mf3.format(argNames + 1, args + 1, 2, result.remove(), errorCode));
 }
 
+void TestMessageFormat::TestMessageFormatNumberSkeleton() {
+    IcuTestErrorCode status(*this, "TestMessageFormatNumberSkeleton");
+
+    static const struct TestCase {
+        const char16_t* messagePattern;
+        const char* localeName;
+        double arg;
+        const char16_t* expected;
+    } cases[] = {
+            { u"{0,number,::percent}", "en", 50, u"50%" },
+            { u"{0,number,::percent scale/100}", "en", 0.5, u"50%" },
+            { u"{0,number,   ::   percent   scale/100   }", "en", 0.5, u"50%" },
+            { u"{0,number,::currency/USD}", "en", 23, u"$23.00" },
+            { u"{0,number,::precision-integer}", "en", 514.23, u"514" },
+            { u"{0,number,::.000}", "en", 514.23, u"514.230" },
+            { u"{0,number,::.}", "en", 514.23, u"514" },
+            { u"{0,number,::}", "fr", 514.23, u"514,23" },
+            { u"Cost: {0,number,::currency/EUR}.", "en", 4.3, u"Cost: €4.30." },
+            { u"{0,number,'::'0.00}", "en", 50, u"::50.00" }, // pattern literal
+    };
+
+    for (auto& cas : cases) {
+        status.setScope(cas.messagePattern);
+        MessageFormat msgf(cas.messagePattern, cas.localeName, status);
+        UnicodeString sb;
+        FieldPosition fpos(0);
+        Formattable argsArray[] = {{cas.arg}};
+        Formattable args(argsArray, 1);
+        msgf.format(args, sb, status);
+
+        assertEquals(cas.messagePattern, cas.expected, sb);
+    }
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */
index 7c0afc411f8a70543afb080df6f536fd69a96c0d..d4bc13d9eaad855c26d48c94baec5e5e93ea8f5e 100644 (file)
@@ -121,6 +121,7 @@ public:
     void TestSelectOrdinal();
     void TestDecimals();
     void TestArgIsPrefixOfAnother();
+    void TestMessageFormatNumberSkeleton();
 
 private:
     UnicodeString GetPatternAndSkipSyntax(const MessagePattern& pattern);
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LocalizedNumberFormatterAsFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LocalizedNumberFormatterAsFormat.java
new file mode 100644 (file)
index 0000000..48f8898
--- /dev/null
@@ -0,0 +1,144 @@
+// © 2018 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.ObjectStreamException;
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+
+import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.number.LocalizedNumberFormatter;
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * A wrapper around LocalizedNumberFormatter implementing the Format interface, enabling improved
+ * compatibility with other APIs. This class is serializable.
+ */
+public class LocalizedNumberFormatterAsFormat extends Format {
+
+    private final transient LocalizedNumberFormatter formatter;
+
+    // Even though the locale is inside the LocalizedNumberFormatter, we have to keep it here, too, because
+    // LocalizedNumberFormatter doesn't have a getLocale() method, and ICU-TC didn't want to add one.
+    private final transient ULocale locale;
+
+    public LocalizedNumberFormatterAsFormat(LocalizedNumberFormatter formatter, ULocale locale) {
+        this.formatter = formatter;
+        this.locale = locale;
+    }
+
+    /**
+     * Formats a Number using the wrapped LocalizedNumberFormatter. The provided object must be a Number.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
+        if (!(obj instanceof Number)) {
+            throw new IllegalArgumentException();
+        }
+        FormattedNumber result = formatter.format((Number) obj);
+        // always return first occurrence:
+        pos.setBeginIndex(0);
+        pos.setEndIndex(0);
+        boolean found = result.nextFieldPosition(pos);
+        if (found && toAppendTo.length() != 0) {
+            pos.setBeginIndex(pos.getBeginIndex() + toAppendTo.length());
+            pos.setEndIndex(pos.getEndIndex() + toAppendTo.length());
+        }
+        result.appendTo(toAppendTo);
+        return toAppendTo;
+    }
+
+    /**
+     * Formats a Number using the wrapped LocalizedNumberFormatter. The provided object must be a Number.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+        if (!(obj instanceof Number)) {
+            throw new IllegalArgumentException();
+        }
+        return formatter.format((Number) obj).toCharacterIterator();
+    }
+
+    /**
+     * Not supported. This method will throw UnsupportedOperationException.
+     */
+    @Override
+    public Object parseObject(String source, ParsePosition pos) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Gets the LocalizedNumberFormatter that this wrapper class uses to format numbers.
+     *
+     * @return The unwrapped LocalizedNumberFormatter.
+     */
+    public LocalizedNumberFormatter getNumberFormatter() {
+        return formatter;
+    }
+
+    @Override
+    public int hashCode() {
+        return formatter.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof LocalizedNumberFormatterAsFormat)) {
+            return false;
+        }
+        return formatter.equals(((LocalizedNumberFormatterAsFormat) other).getNumberFormatter());
+    }
+
+    private Object writeReplace() throws ObjectStreamException {
+        Proxy proxy = new Proxy();
+        proxy.languageTag = locale.toLanguageTag();
+        proxy.skeleton = formatter.toSkeleton();
+        return proxy;
+    }
+
+    static class Proxy implements Externalizable {
+        String languageTag;
+        String skeleton;
+
+        // Must have public constructor, to enable Externalizable
+        public Proxy() {
+        }
+
+        @Override
+        public void writeExternal(ObjectOutput out) throws IOException {
+            out.writeByte(0); // version
+            out.writeUTF(languageTag);
+            out.writeUTF(skeleton);
+        }
+
+        @Override
+        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+            in.readByte(); // version
+            languageTag = in.readUTF();
+            skeleton = in.readUTF();
+        }
+
+        private Object readResolve() throws ObjectStreamException {
+            return NumberFormatter.fromSkeleton(skeleton)
+                    .locale(ULocale.forLanguageTag(languageTag))
+                    .toFormat();
+        }
+    }
+}
index 245447f6da62ff8e2e88d58bd70685439d489576..7077a97876ed4ef1080e29e630f0fb005ae83442 100644 (file)
@@ -3,12 +3,14 @@
 package com.ibm.icu.number;
 
 import java.math.BigInteger;
+import java.text.Format;
 import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 
 import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
+import com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat;
 import com.ibm.icu.impl.number.MacroProps;
 import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.math.BigDecimal;
@@ -114,6 +116,23 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
         return withUnit.format(number);
     }
 
+    /**
+     * Creates a representation of this LocalizedNumberFormat as a {@link java.text.Format}, enabling the
+     * use of this number formatter with APIs that need an object of that type, such as MessageFormat.
+     * <p>
+     * This API is not intended to be used other than for enabling API compatibility. The {@link #format}
+     * methods should normally be used when formatting numbers, not the Format object returned by this
+     * method.
+     *
+     * @return A Format wrapping this LocalizedNumberFormatter.
+     * @draft ICU 62
+     * @provisional This API might change or be removed in a future release.
+     * @see NumberFormatter
+     */
+    public Format toFormat() {
+        return new LocalizedNumberFormatterAsFormat(this, resolve().loc);
+    }
+
     /**
      * This is the core entrypoint to the number formatting pipeline. It performs self-regulation: a
      * static code path for the first few calls, and compiling a more efficient data structure if called
index 08e5a9d2700cd1f52c01121c40aee813c6ff3969..53c81be55bf2f8e7cf30b977af4a1dcad32f0662 100644 (file)
@@ -36,6 +36,7 @@ import java.util.Set;
 
 import com.ibm.icu.impl.PatternProps;
 import com.ibm.icu.impl.Utility;
+import com.ibm.icu.number.NumberFormatter;
 import com.ibm.icu.text.MessagePattern.ArgType;
 import com.ibm.icu.text.MessagePattern.Part;
 import com.ibm.icu.text.PluralRules.IFixedDecimal;
@@ -2204,9 +2205,17 @@ public class MessageFormat extends UFormat {
             case MODIFIER_INTEGER:
                 newFormat = NumberFormat.getIntegerInstance(ulocale);
                 break;
-            default: // pattern
-                newFormat = new DecimalFormat(style,
-                        new DecimalFormatSymbols(ulocale));
+            default: // pattern or skeleton
+                // Ignore leading whitespace when looking for "::", the skeleton signal sequence
+                int i = 0;
+                for (; PatternProps.isWhiteSpace(style.charAt(i)); i++);
+                if (style.regionMatches(i, "::", 0, 2)) {
+                    // Skeleton
+                    newFormat = NumberFormatter.fromSkeleton(style.substring(i + 2)).locale(ulocale).toFormat();
+                } else {
+                    // Pattern
+                    newFormat = new DecimalFormat(style, new DecimalFormatSymbols(ulocale));
+                }
                 break;
             }
             break;
index 28a574314654201867fd8fc31e051577430058f7..db2b40c0da94c2c7e5fadee47a83c0ec0b4b1334 100644 (file)
@@ -894,7 +894,7 @@ public class MessageRegressionTest extends TestFmwk {
         format2 = serializeAndDeserialize(format1);
         assertEquals("MessageFormats (empty pattern) before and after serialization are not equal", format1, format2);
 
-        format1.applyPattern("ab{1}cd{0,number}ef{3,date}gh");
+        format1.applyPattern("ab{1}cd{0,number}ef{3,date}gh{4,number,::percent}ij");
         format1.setFormat(2, null);
         format1.setFormatByArgumentIndex(1, NumberFormat.getInstance(ULocale.ENGLISH));
         format2 = serializeAndDeserialize(format1);
@@ -902,7 +902,7 @@ public class MessageRegressionTest extends TestFmwk {
         assertEquals(
                 "MessageFormat (with custom formats) does not "+
                 "format correctly after serialization",
-                "ab3.3cd4,4ef***gh",
-                format2.format(new Object[] { 4.4, 3.3, "+++", "***" }));
+                "ab3.3cd4,4ef***gh50\u00A0%ij",
+                format2.format(new Object[] { 4.4, 3.3, "+++", "***", 50 }));
     }
 }
index 83dc828937f5a6e83c463727723555b63ae0160d..ff2ebdc1365539e8f65db5456ef51f4294966bba 100644 (file)
@@ -2092,4 +2092,34 @@ public class TestMessageFormat extends TestFmwk {
         int actualHashResult2 = testDF2.hashCode();
         assertNotEquals("DateFormat hashCode() test: really the same hashcode?", actualHashResult1, actualHashResult2);
     }
+
+    @Test
+    public void TestMessageFormatNumberSkeleton() {
+        Object[][] cases = new Object[][] {
+                { "{0,number,::percent}", ULocale.ENGLISH, 50, "50%" },
+                { "{0,number,::percent scale/100}", ULocale.ENGLISH, 0.5, "50%" },
+                { "{0,number,   ::   percent   scale/100   }", ULocale.ENGLISH, 0.5, "50%" },
+                { "{0,number,::currency/USD}", ULocale.ENGLISH, 23, "$23.00" },
+                { "{0,number,::precision-integer}", ULocale.ENGLISH, 514.23, "514" },
+                { "{0,number,::.000}", ULocale.ENGLISH, 514.23, "514.230" },
+                { "{0,number,::.}", ULocale.ENGLISH, 514.23, "514" },
+                { "{0,number,::}", ULocale.FRENCH, 514.23, "514,23" },
+                { "Cost: {0,number,::currency/EUR}.", ULocale.ENGLISH, 4.3, "Cost: €4.30." },
+                { "{0,number,'::'0.00}", ULocale.ENGLISH, 50, "::50.00" }, // pattern literal
+        };
+
+        for (Object[] cas : cases) {
+            String messagePattern = (String) cas[0];
+            ULocale locale = (ULocale) cas[1];
+            Number arg = (Number) cas[2];
+            String expected = (String) cas[3];
+
+            MessageFormat msgf = new MessageFormat(messagePattern, locale);
+            StringBuffer sb = new StringBuffer();
+            FieldPosition fpos = new FieldPosition(0);
+            msgf.format(new Object[] { arg }, sb, fpos);
+
+            assertEquals(messagePattern, expected, sb.toString());
+        }
+    }
 }
index 5b99460b8feb1f98b1c95212ab5c18bbfebcaa96..41ab32391914e1697d4321483fa93bd59b4746fe 100644 (file)
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
+import java.text.Format;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
@@ -23,7 +24,9 @@ import java.util.Set;
 import org.junit.Ignore;
 import org.junit.Test;
 
+import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
 import com.ibm.icu.impl.number.Grouper;
+import com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat;
 import com.ibm.icu.impl.number.MacroProps;
 import com.ibm.icu.impl.number.Padder;
 import com.ibm.icu.impl.number.Padder.PadPosition;
@@ -2118,6 +2121,47 @@ public class NumberFormatterApiTest {
         assertFalse("No fraction part in an integer", fmtd.nextFieldPosition(actual));
     }
 
+    /** Handler for serialization compatibility test suite. */
+    public static class FormatHandler implements SerializableTestUtility.Handler {
+        @Override
+        public Object[] getTestObjects() {
+            return new Object[] {
+                    NumberFormatter.withLocale(ULocale.FRENCH).toFormat(),
+                    NumberFormatter.fromSkeleton("percent").locale(ULocale.JAPANESE).toFormat(),
+                    NumberFormatter.fromSkeleton("scientific .000").locale(ULocale.ENGLISH).toFormat() };
+        }
+
+        @Override
+        public boolean hasSameBehavior(Object a, Object b) {
+            LocalizedNumberFormatterAsFormat f1 = (LocalizedNumberFormatterAsFormat) a;
+            LocalizedNumberFormatterAsFormat f2 = (LocalizedNumberFormatterAsFormat) b;
+            String s1 = f1.format(514.23);
+            String s2 = f1.format(514.23);
+            String k1 = f1.getNumberFormatter().toSkeleton();
+            String k2 = f2.getNumberFormatter().toSkeleton();
+            return s1.equals(s2) && k1.equals(k2);
+        }
+    }
+
+    @Test
+    public void toFormat() {
+        LocalizedNumberFormatter lnf = NumberFormatter.withLocale(ULocale.FRENCH)
+                .precision(Precision.fixedFraction(3));
+        Format format = lnf.toFormat();
+        FieldPosition fpos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
+        StringBuffer sb = new StringBuffer();
+        format.format(514.23, sb, fpos);
+        assertEquals("Should correctly format number", "514,230", sb.toString());
+        assertEquals("Should find decimal separator", 3, fpos.getBeginIndex());
+        assertEquals("Should find end of decimal separator", 4, fpos.getEndIndex());
+        assertEquals("LocalizedNumberFormatter should round-trip",
+                lnf,
+                ((LocalizedNumberFormatterAsFormat) format).getNumberFormatter());
+        assertEquals("Should produce same character iterator",
+                lnf.format(514.23).toCharacterIterator().getAttributes(),
+                format.formatToCharacterIterator(514.23).getAttributes());
+    }
+
     @Test
     public void plurals() {
         // TODO: Expand this test.
index e3ffd019857d87d2552f38888d87be8a7ab63169..c4c6a3f0bad4b0692dc976099eea5256e3550d1c 100644 (file)
@@ -30,6 +30,7 @@ import java.util.Locale;
 
 import com.ibm.icu.dev.test.format.MeasureUnitTest;
 import com.ibm.icu.dev.test.format.PluralRulesTest;
+import com.ibm.icu.dev.test.number.NumberFormatterApiTest;
 import com.ibm.icu.dev.test.number.PropertiesTest;
 import com.ibm.icu.impl.JavaTimeZone;
 import com.ibm.icu.impl.OlsonTimeZone;
@@ -833,6 +834,7 @@ public class SerializableTestUtility {
         map.put("com.ibm.icu.impl.number.DecimalFormatProperties", new PropertiesTest.PropertiesHandler());
         map.put("com.ibm.icu.impl.number.CustomSymbolCurrency", new CurrencyHandler());
         map.put("com.ibm.icu.number.SkeletonSyntaxException", new ExceptionHandler.SkeletonSyntaxExceptionHandler());
+        map.put("com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat", new NumberFormatterApiTest.FormatHandler());
 
         map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler());
         map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());