From b347a140ec8d88637a415ec5b22513ae42176b02 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Tue, 15 May 2018 00:05:04 +0000 Subject: [PATCH] ICU-13742 Implementing number skeletons in MessageFormat. X-SVN-Rev: 41377 --- icu4c/source/i18n/msgfmt.cpp | 22 ++- icu4c/source/i18n/number_fluent.cpp | 42 ++--- icu4c/source/i18n/number_utils.cpp | 80 ++++++++++ icu4c/source/i18n/number_utils.h | 76 +++++++++ icu4c/source/i18n/unicode/numberformatter.h | 15 ++ icu4c/source/test/intltest/numbertest.h | 1 + icu4c/source/test/intltest/numbertest_api.cpp | 26 ++++ icu4c/source/test/intltest/tmsgfmt.cpp | 35 +++++ icu4c/source/test/intltest/tmsgfmt.h | 1 + .../LocalizedNumberFormatterAsFormat.java | 144 ++++++++++++++++++ .../icu/number/LocalizedNumberFormatter.java | 19 +++ .../src/com/ibm/icu/text/MessageFormat.java | 15 +- .../test/format/MessageRegressionTest.java | 6 +- .../dev/test/format/TestMessageFormat.java | 30 ++++ .../test/number/NumberFormatterApiTest.java | 44 ++++++ .../serializable/SerializableTestUtility.java | 2 + 16 files changed, 529 insertions(+), 29 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/LocalizedNumberFormatterAsFormat.java diff --git a/icu4c/source/i18n/msgfmt.cpp b/icu4c/source/i18n/msgfmt.cpp index faf6c21c6eb..0287da699cb 100644 --- a/icu4c/source/i18n/msgfmt.cpp +++ b/icu4c/source/i18n/msgfmt.cpp @@ -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(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(fmt); + if (decfmt != nullptr) { + decfmt->applyPattern(style, parseError, ec); + } } } break; diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index 0d3ee2d1238..a1915fdcbc0 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -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 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); diff --git a/icu4c/source/i18n/number_utils.cpp b/icu4c/source/i18n/number_utils.cpp index 2a74dba5ab1..da653e4a6d9 100644 --- a/icu4c/source/i18n/number_utils.cpp +++ b/icu4c/source/i18n/number_utils.cpp @@ -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(&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; diff --git a/icu4c/source/i18n/number_utils.h b/icu4c/source/i18n/number_utils.h index df6f0d3080f..86f56983a40 100644 --- a/icu4c/source/i18n/number_utils.h +++ b/icu4c/source/i18n/number_utils.h @@ -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: + * + *
+     * LocalizedNumberFormatter localFormatter = fmt->getNumberFormatter();
+     * 
+ * + * You can however use the return value directly when chaining: + * + *
+     * FormattedNumber result = fmt->getNumberFormatter().formatDouble(514.23, status);
+     * 
+ * + * @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, diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index e44259c6e8c..3fc77d83f11 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -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. * diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index e6891960880..69c3ee44edc 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -68,6 +68,7 @@ class NumberFormatterApiTest : public IntlTest { void locale(); void formatTypes(); void fieldPosition(); + void toFormat(); void errors(); void validRanges(); void copyMove(); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index b8ddd44b9f3..e74d4958d4d 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -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(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(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( diff --git a/icu4c/source/test/intltest/tmsgfmt.cpp b/icu4c/source/test/intltest/tmsgfmt.cpp index 72557fe8d6f..3a16c285ec4 100644 --- a/icu4c/source/test/intltest/tmsgfmt.cpp +++ b/icu4c/source/test/intltest/tmsgfmt.cpp @@ -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 */ diff --git a/icu4c/source/test/intltest/tmsgfmt.h b/icu4c/source/test/intltest/tmsgfmt.h index 7c0afc411f8..d4bc13d9eaa 100644 --- a/icu4c/source/test/intltest/tmsgfmt.h +++ b/icu4c/source/test/intltest/tmsgfmt.h @@ -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 index 00000000000..48f8898b4cd --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LocalizedNumberFormatterAsFormat.java @@ -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(); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java index 245447f6da6..7077a97876e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java @@ -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 + * 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 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 08e5a9d2700..53c81be55bf 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 @@ -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; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessageRegressionTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessageRegressionTest.java index 28a57431465..db2b40c0da9 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessageRegressionTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessageRegressionTest.java @@ -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 })); } } 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 83dc828937f..ff2ebdc1365 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 @@ -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()); + } + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java index 5b99460b8fe..41ab3239191 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java @@ -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. diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java index e3ffd019857..c4c6a3f0bad 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java @@ -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());