From 1f85e940682077ab62ceb8eae1aa9a6344119d2e Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Thu, 10 Jan 2019 23:42:43 -0800 Subject: [PATCH] ICU-13839 Adding FormattedNumber API to PluralRules. - Makes new dependency class for PluralRules+FormattedNumber. --- icu4c/source/i18n/Makefile.in | 2 +- icu4c/source/i18n/i18n.vcxproj | 1 + icu4c/source/i18n/i18n.vcxproj.filters | 3 + icu4c/source/i18n/i18n_uwp.vcxproj | 1 + icu4c/source/i18n/number_capi.cpp | 10 ++ icu4c/source/i18n/number_fluent.cpp | 107 --------------- icu4c/source/i18n/number_output.cpp | 128 ++++++++++++++++++ icu4c/source/i18n/number_utypes.h | 5 + icu4c/source/i18n/plurrule.cpp | 11 ++ icu4c/source/i18n/unicode/plurrule.h | 33 ++++- icu4c/source/i18n/unicode/upluralrules.h | 38 +++++- icu4c/source/i18n/upluralrules.cpp | 24 ++++ icu4c/source/test/cintltst/cpluralrulestest.c | 43 ++++++ icu4c/source/test/depstest/dependencies.txt | 18 ++- icu4c/source/test/intltest/plurults.cpp | 35 ++++- icu4c/source/test/intltest/plurults.h | 1 + .../src/com/ibm/icu/text/PluralRules.java | 24 +++- .../icu/dev/test/format/PluralRulesTest.java | 88 ++++++++---- 18 files changed, 422 insertions(+), 150 deletions(-) create mode 100644 icu4c/source/i18n/number_output.cpp diff --git a/icu4c/source/i18n/Makefile.in b/icu4c/source/i18n/Makefile.in index 65705929b44..03617fe1b64 100644 --- a/icu4c/source/i18n/Makefile.in +++ b/icu4c/source/i18n/Makefile.in @@ -101,7 +101,7 @@ uregion.o reldatefmt.o quantityformatter.o measunit.o \ sharedbreakiterator.o scientificnumberformatter.o dayperiodrules.o nounit.o \ number_affixutils.o number_compact.o number_decimalquantity.o \ number_decimfmtprops.o number_fluent.o number_formatimpl.o number_grouping.o \ -number_integerwidth.o number_longnames.o number_modifiers.o number_notation.o \ +number_integerwidth.o number_longnames.o number_modifiers.o number_notation.o number_output.o \ number_padding.o number_patternmodifier.o number_patternstring.o \ number_rounding.o number_scientific.o number_stringbuilder.o number_utils.o number_asformat.o \ number_mapper.o number_multiplier.o number_currencysymbols.o number_skeletons.o number_capi.o \ diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index 57cdb8da7be..4cccb5f3e6d 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -270,6 +270,7 @@ + diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters index dc90bfce517..ee6850555dc 100644 --- a/icu4c/source/i18n/i18n.vcxproj.filters +++ b/icu4c/source/i18n/i18n.vcxproj.filters @@ -561,6 +561,9 @@ formatting + + formatting + formatting diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index 9db4848ed33..93ae76c8a34 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -377,6 +377,7 @@ + diff --git a/icu4c/source/i18n/number_capi.cpp b/icu4c/source/i18n/number_capi.cpp index 5e196a262fb..c39c69f4cea 100644 --- a/icu4c/source/i18n/number_capi.cpp +++ b/icu4c/source/i18n/number_capi.cpp @@ -61,6 +61,16 @@ UFormattedNumberImpl::~UFormattedNumberImpl() { U_NAMESPACE_END +const DecimalQuantity* icu::number::impl::validateUFormattedNumberToDecimalQuantity( + const UFormattedNumber* uresult, UErrorCode& status) { + auto* result = UFormattedNumberApiHelper::validate(uresult, status); + if (U_FAILURE(status)) { + return nullptr; + } + return &result->fData.quantity; +} + + U_CAPI UNumberFormatter* U_EXPORT2 unumf_openForSkeletonAndLocale(const UChar* skeleton, int32_t skeletonLen, const char* locale, diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index 5bc1f982fa6..84e7d05ac59 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -770,111 +770,4 @@ Format* LocalizedNumberFormatter::toFormat(UErrorCode& status) 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; -} - -UnicodeString FormattedNumber::toString(UErrorCode& status) const { - if (U_FAILURE(status)) { - return ICU_Utility::makeBogusString(); - } - if (fResults == nullptr) { - status = fErrorCode; - return ICU_Utility::makeBogusString(); - } - return fResults->string.toUnicodeString(); -} - -UnicodeString FormattedNumber::toTempString(UErrorCode& status) const { - if (U_FAILURE(status)) { - return ICU_Utility::makeBogusString(); - } - if (fResults == nullptr) { - status = fErrorCode; - return ICU_Utility::makeBogusString(); - } - return fResults->string.toTempUnicodeString(); -} - -Appendable& FormattedNumber::appendTo(Appendable& appendable, UErrorCode& status) const { - if (U_FAILURE(status)) { - return appendable; - } - if (fResults == nullptr) { - status = fErrorCode; - return appendable; - } - appendable.appendString(fResults->string.chars(), fResults->string.length()); - return appendable; -} - -UBool FormattedNumber::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const { - if (U_FAILURE(status)) { - return FALSE; - } - if (fResults == nullptr) { - status = fErrorCode; - return FALSE; - } - // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool - return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; -} - -UBool FormattedNumber::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const { - if (U_FAILURE(status)) { - return FALSE; - } - if (fResults == nullptr) { - status = fErrorCode; - return FALSE; - } - // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool - return fResults->string.nextFieldPosition(fieldPosition, status) ? TRUE : FALSE; -} - -void FormattedNumber::getAllFieldPositions(FieldPositionIterator& iterator, UErrorCode& status) const { - FieldPositionIteratorHandler fpih(&iterator, status); - getAllFieldPositionsImpl(fpih, status); -} - -void FormattedNumber::getAllFieldPositionsImpl(FieldPositionIteratorHandler& fpih, - UErrorCode& status) const { - if (U_FAILURE(status)) { - return; - } - if (fResults == nullptr) { - status = fErrorCode; - return; - } - fResults->string.getAllFieldPositions(fpih, status); -} - -void FormattedNumber::getDecimalQuantity(DecimalQuantity& output, UErrorCode& status) const { - if (U_FAILURE(status)) { - return; - } - if (fResults == nullptr) { - status = fErrorCode; - return; - } - output = fResults->quantity; -} - -FormattedNumber::~FormattedNumber() { - delete fResults; -} - #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_output.cpp b/icu4c/source/i18n/number_output.cpp new file mode 100644 index 00000000000..e39f5756845 --- /dev/null +++ b/icu4c/source/i18n/number_output.cpp @@ -0,0 +1,128 @@ +// © 2019 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "unicode/numberformatter.h" +#include "number_utypes.h" +#include "util.h" +#include "number_decimalquantity.h" + +U_NAMESPACE_BEGIN +namespace number { + + +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(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->string.toUnicodeString(); +} + +UnicodeString FormattedNumber::toTempString(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->string.toTempUnicodeString(); +} + +Appendable& FormattedNumber::appendTo(Appendable& appendable, UErrorCode& status) const { + if (U_FAILURE(status)) { + return appendable; + } + if (fResults == nullptr) { + status = fErrorCode; + return appendable; + } + appendable.appendString(fResults->string.chars(), fResults->string.length()); + return appendable; +} + +UBool FormattedNumber::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const { + if (U_FAILURE(status)) { + return FALSE; + } + if (fResults == nullptr) { + status = fErrorCode; + return FALSE; + } + // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool + return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; +} + +UBool FormattedNumber::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const { + if (U_FAILURE(status)) { + return FALSE; + } + if (fResults == nullptr) { + status = fErrorCode; + return FALSE; + } + // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool + return fResults->string.nextFieldPosition(fieldPosition, status) ? TRUE : FALSE; +} + +void FormattedNumber::getAllFieldPositions(FieldPositionIterator& iterator, UErrorCode& status) const { + FieldPositionIteratorHandler fpih(&iterator, status); + getAllFieldPositionsImpl(fpih, status); +} + +void FormattedNumber::getAllFieldPositionsImpl(FieldPositionIteratorHandler& fpih, + UErrorCode& status) const { + if (U_FAILURE(status)) { + return; + } + if (fResults == nullptr) { + status = fErrorCode; + return; + } + fResults->string.getAllFieldPositions(fpih, status); +} + +void FormattedNumber::getDecimalQuantity(impl::DecimalQuantity& output, UErrorCode& status) const { + if (U_FAILURE(status)) { + return; + } + if (fResults == nullptr) { + status = fErrorCode; + return; + } + output = fResults->quantity; +} + +FormattedNumber::~FormattedNumber() { + delete fResults; +} + + +} // namespace number +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_utypes.h b/icu4c/source/i18n/number_utypes.h index 18917dea514..56e0fa8ebaa 100644 --- a/icu4c/source/i18n/number_utypes.h +++ b/icu4c/source/i18n/number_utypes.h @@ -27,6 +27,11 @@ struct UFormattedValueImpl : public UMemory, public UFormattedValueApiHelper { }; +/** Helper function used in upluralrules.cpp */ +const DecimalQuantity* validateUFormattedNumberToDecimalQuantity( + const UFormattedNumber* uresult, UErrorCode& status); + + /** * Struct for data used by FormattedNumber. * diff --git a/icu4c/source/i18n/plurrule.cpp b/icu4c/source/i18n/plurrule.cpp index 90876781d54..e82c02cb592 100644 --- a/icu4c/source/i18n/plurrule.cpp +++ b/icu4c/source/i18n/plurrule.cpp @@ -35,6 +35,7 @@ #include "sharedpluralrules.h" #include "unifiedcache.h" #include "number_decimalquantity.h" +#include "util.h" #if !UCONFIG_NO_FORMATTING @@ -264,6 +265,16 @@ PluralRules::select(double number) const { return select(FixedDecimal(number)); } +UnicodeString +PluralRules::select(const number::FormattedNumber& number, UErrorCode& status) const { + DecimalQuantity dq; + number.getDecimalQuantity(dq, status); + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + return select(dq); +} + UnicodeString PluralRules::select(const IFixedDecimal &number) const { if (mRules == nullptr) { diff --git a/icu4c/source/i18n/unicode/plurrule.h b/icu4c/source/i18n/unicode/plurrule.h index daeed52bee6..55167136a08 100644 --- a/icu4c/source/i18n/unicode/plurrule.h +++ b/icu4c/source/i18n/unicode/plurrule.h @@ -50,6 +50,10 @@ class PluralKeywordEnumeration; class AndConstraint; class SharedPluralRules; +namespace number { +class FormattedNumber; +} + /** * Defines rules for mapping non-negative numeric values onto a small set of * keywords. Rules are constructed from a text description, consisting @@ -323,9 +327,9 @@ public: #endif /* U_HIDE_INTERNAL_API */ /** - * Given a number, returns the keyword of the first rule that applies to - * the number. This function can be used with isKeyword* functions to - * determine the keyword for default plural rules. + * Given an integer, returns the keyword of the first rule + * that applies to the number. This function can be used with + * isKeyword* functions to determine the keyword for default plural rules. * * @param number The number for which the rule has to be determined. * @return The keyword of the selected rule. @@ -334,9 +338,9 @@ public: UnicodeString select(int32_t number) const; /** - * Given a number, returns the keyword of the first rule that applies to - * the number. This function can be used with isKeyword* functions to - * determine the keyword for default plural rules. + * Given a floating-point number, returns the keyword of the first rule + * that applies to the number. This function can be used with + * isKeyword* functions to determine the keyword for default plural rules. * * @param number The number for which the rule has to be determined. * @return The keyword of the selected rule. @@ -344,6 +348,23 @@ public: */ UnicodeString select(double number) const; + /** + * Given a formatted number, returns the keyword of the first rule + * that applies to the number. This function can be used with + * isKeyword* functions to determine the keyword for default plural rules. + * + * A FormattedNumber allows you to specify an exponent or trailing zeros, + * which can affect the plural category. To get a FormattedNumber, see + * NumberFormatter. + * + * @param number The number for which the rule has to be determined. + * @param status Set if an error occurs while selecting plural keyword. + * This could happen if the FormattedNumber is invalid. + * @return The keyword of the selected rule. + * @draft ICU 64 + */ + UnicodeString select(const number::FormattedNumber& number, UErrorCode& status) const; + #ifndef U_HIDE_INTERNAL_API /** * @internal diff --git a/icu4c/source/i18n/unicode/upluralrules.h b/icu4c/source/i18n/unicode/upluralrules.h index 690846bc89c..7c51e47aeae 100644 --- a/icu4c/source/i18n/unicode/upluralrules.h +++ b/icu4c/source/i18n/unicode/upluralrules.h @@ -20,6 +20,9 @@ #include "unicode/unum.h" #endif /* U_HIDE_INTERNAL_API */ +// Forward-declaration +struct UFormattedNumber; + /** * \file * \brief C API: Plural rules, select plural keywords for numeric values. @@ -132,14 +135,15 @@ U_NAMESPACE_END /** - * Given a number, returns the keyword of the first rule that + * Given a floating-point number, returns the keyword of the first rule that * applies to the number, according to the supplied UPluralRules object. * @param uplrules The UPluralRules object specifying the rules. * @param number The number for which the rule has to be determined. - * @param keyword The keyword of the rule that applies to number. - * @param capacity The capacity of keyword. + * @param keyword An output buffer to write the keyword of the rule that + * applies to number. + * @param capacity The capacity of the keyword buffer. * @param status A pointer to a UErrorCode to receive any errors. - * @return The length of keyword. + * @return The length of the keyword. * @stable ICU 4.8 */ U_CAPI int32_t U_EXPORT2 @@ -148,6 +152,29 @@ uplrules_select(const UPluralRules *uplrules, UChar *keyword, int32_t capacity, UErrorCode *status); +/** + * Given a formatted number, returns the keyword of the first rule + * that applies to the number, according to the supplied UPluralRules object. + * + * A UFormattedNumber allows you to specify an exponent or trailing zeros, + * which can affect the plural category. To get a UFormattedNumber, see + * {@link UNumberFormatter}. + * + * @param uplrules The UPluralRules object specifying the rules. + * @param number The formatted number for which the rule has to be determined. + * @param keyword The destination buffer for the keyword of the rule that + * applies to number. + * @param capacity The capacity of the keyword buffer. + * @param status A pointer to a UErrorCode to receive any errors. + * @return The length of the keyword. + * @draft ICU 64 + */ +U_CAPI int32_t U_EXPORT2 +uplrules_selectFormatted(const UPluralRules *uplrules, + const struct UFormattedNumber* number, + UChar *keyword, int32_t capacity, + UErrorCode *status); + #ifndef U_HIDE_INTERNAL_API /** * Given a number, returns the keyword of the first rule that applies to the @@ -160,7 +187,8 @@ uplrules_select(const UPluralRules *uplrules, * @param fmt The UNumberFormat specifying how the number will be formatted * (this can affect the plural form, e.g. "1 dollar" vs "1.0 dollars"). * If this is NULL, the function behaves like uplrules_select. - * @param keyword The keyword of the rule that applies to number. + * @param keyword An output buffer to write the keyword of the rule that + * applies to number. * @param capacity The capacity of the keyword buffer. * @param status A pointer to a UErrorCode to receive any errors. * @return The length of keyword. diff --git a/icu4c/source/i18n/upluralrules.cpp b/icu4c/source/i18n/upluralrules.cpp index bba6dfe3101..5119257fd80 100644 --- a/icu4c/source/i18n/upluralrules.cpp +++ b/icu4c/source/i18n/upluralrules.cpp @@ -17,7 +17,9 @@ #include "unicode/unistr.h" #include "unicode/unum.h" #include "unicode/numfmt.h" +#include "unicode/unumberformatter.h" #include "number_decimalquantity.h" +#include "number_utypes.h" U_NAMESPACE_USE @@ -91,6 +93,28 @@ uplrules_select(const UPluralRules *uplrules, return result.extract(keyword, capacity, *status); } +U_CAPI int32_t U_EXPORT2 +uplrules_selectFormatted(const UPluralRules *uplrules, + const UFormattedNumber* number, + UChar *keyword, int32_t capacity, + UErrorCode *status) +{ + if (U_FAILURE(*status)) { + return 0; + } + if (keyword == NULL ? capacity != 0 : capacity < 0) { + *status = U_ILLEGAL_ARGUMENT_ERROR; + return 0; + } + const number::impl::DecimalQuantity* dq = + number::impl::validateUFormattedNumberToDecimalQuantity(number, *status); + if (U_FAILURE(*status)) { + return 0; + } + UnicodeString result = ((PluralRules*)uplrules)->select(*dq); + return result.extract(keyword, capacity, *status); +} + U_CAPI int32_t U_EXPORT2 uplrules_selectWithFormat(const UPluralRules *uplrules, double number, diff --git a/icu4c/source/test/cintltst/cpluralrulestest.c b/icu4c/source/test/cintltst/cpluralrulestest.c index 24b65433f69..8db9d1fb889 100644 --- a/icu4c/source/test/cintltst/cpluralrulestest.c +++ b/icu4c/source/test/cintltst/cpluralrulestest.c @@ -13,6 +13,7 @@ #include "unicode/upluralrules.h" #include "unicode/ustring.h" #include "unicode/uenum.h" +#include "unicode/unumberformatter.h" #include "cintltst.h" #include "cmemory.h" #include "cstring.h" @@ -20,6 +21,7 @@ static void TestPluralRules(void); static void TestOrdinalRules(void); static void TestGetKeywords(void); +static void TestFormatted(void); void addPluralRulesTest(TestNode** root); @@ -30,6 +32,7 @@ void addPluralRulesTest(TestNode** root) TESTCASE(TestPluralRules); TESTCASE(TestOrdinalRules); TESTCASE(TestGetKeywords); + TESTCASE(TestFormatted); } typedef struct { @@ -252,4 +255,44 @@ static void TestGetKeywords() { } } +static void TestFormatted() { + UErrorCode ec = U_ZERO_ERROR; + UNumberFormatter* unumf = NULL; + UFormattedNumber* uresult = NULL; + UPluralRules* uplrules = NULL; + + uplrules = uplrules_open("hr", &ec); + if (!assertSuccess("open plural rules", &ec)) { + goto cleanup; + } + + unumf = unumf_openForSkeletonAndLocale(u".00", -1, "hr", &ec); + if (!assertSuccess("open unumf", &ec)) { + goto cleanup; + } + + uresult = unumf_openResult(&ec); + if (!assertSuccess("open result", &ec)) { + goto cleanup; + } + + unumf_formatDouble(unumf, 100.2, uresult, &ec); + if (!assertSuccess("format", &ec)) { + goto cleanup; + } + + UChar buffer[40]; + uplrules_selectFormatted(uplrules, uresult, buffer, 40, &ec); + if (!assertSuccess("select", &ec)) { + goto cleanup; + } + + assertUEquals("0.20 is plural category 'other' in hr", u"other", buffer); + +cleanup: + uplrules_close(uplrules); + unumf_close(unumf); + unumf_closeResult(uresult); +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index a54f024f1ff..423469f7f26 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -840,7 +840,7 @@ library: i18n dayperiodrules listformatter formatting formattable_cnv regex regex_cnv translit - double_conversion number_representation numberformatter numberparser + double_conversion number_representation number_output numberformatter numberparser universal_time_scale uclean_i18n @@ -937,6 +937,16 @@ group: number_representation ucase uniset_core formatted_value +group: number_output + # PluralRules and FormattedNumber + number_output.o + standardplural.o plurrule.o + deps + # FormattedNumber internals: + number_representation format + # PluralRules internals: + unifiedcache + group: numberformatter # ICU 60+ NumberFormatter API number_affixutils.o number_asformat.o @@ -950,11 +960,9 @@ group: numberformatter number_scientific.o number_skeletons.o currpinf.o dcfmtsym.o numsys.o numrange_fluent.o numrange_impl.o - # pluralrules - standardplural.o plurrule.o deps - decnumber double_conversion formattable format units - number_representation + decnumber double_conversion formattable units + number_representation number_output uclean_i18n common group: numberparser diff --git a/icu4c/source/test/intltest/plurults.cpp b/icu4c/source/test/intltest/plurults.cpp index 80ce09a7498..0d59a5a6703 100644 --- a/icu4c/source/test/intltest/plurults.cpp +++ b/icu4c/source/test/intltest/plurults.cpp @@ -6,7 +6,7 @@ * others. All Rights Reserved. ******************************************************************************** -* File PLURRULTS.cpp +* File PLURULTS.cpp * ******************************************************************************** */ @@ -22,6 +22,7 @@ #include "unicode/localpointer.h" #include "unicode/plurrule.h" #include "unicode/stringpiece.h" +#include "unicode/numberformatter.h" #include "cmemory.h" #include "plurrule_impl.h" @@ -53,6 +54,7 @@ void PluralRulesTest::runIndexedTest( int32_t index, UBool exec, const char* &na TESTCASE_AUTO(testAvailbleLocales); TESTCASE_AUTO(testParseErrors); TESTCASE_AUTO(testFixedDecimal); + TESTCASE_AUTO(testSelectTrailingZeros); TESTCASE_AUTO_END; } @@ -1004,5 +1006,36 @@ void PluralRulesTest::testFixedDecimal() { } +void PluralRulesTest::testSelectTrailingZeros() { + IcuTestErrorCode status(*this, "testSelectTrailingZeros"); + number::UnlocalizedNumberFormatter unf = number::NumberFormatter::with() + .precision(number::Precision::fixedFraction(2)); + struct TestCase { + const char* localeName; + const char16_t* expectedDoubleKeyword; + const char16_t* expectedFormattedKeyword; + double number; + } cases[] = { + {"bs", u"few", u"other", 5.2}, // 5.2 => two, but 5.20 => other + {"si", u"one", u"one", 0.0}, + {"si", u"one", u"one", 1.0}, + {"si", u"one", u"other", 0.1}, // 0.1 => one, but 0.10 => other + {"si", u"one", u"one", 0.01}, // 0.01 => one + {"hsb", u"few", u"few", 1.03}, // (f % 100 == 3) => few + {"hsb", u"few", u"other", 1.3}, // 1.3 => few, but 1.30 => other + }; + for (const auto& cas : cases) { + UnicodeString message(UnicodeString(cas.localeName) + u" " + DoubleToUnicodeString(cas.number)); + status.setScope(message); + Locale locale(cas.localeName); + LocalPointer rules(PluralRules::forLocale(locale, status)); + assertEquals(message, cas.expectedDoubleKeyword, rules->select(cas.number)); + number::FormattedNumber fn = unf.locale(locale).formatDouble(cas.number, status); + assertEquals(message, cas.expectedFormattedKeyword, rules->select(fn, status)); + status.errIfFailureAndReset(); + } +} + + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/plurults.h b/icu4c/source/test/intltest/plurults.h index 2189593aad1..e4e2497ec69 100644 --- a/icu4c/source/test/intltest/plurults.h +++ b/icu4c/source/test/intltest/plurults.h @@ -37,6 +37,7 @@ private: void testAvailbleLocales(); void testParseErrors(); void testFixedDecimal(); + void testSelectTrailingZeros(); void assertRuleValue(const UnicodeString& rule, double expected); void assertRuleKeyValue(const UnicodeString& rule, const UnicodeString& key, diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index 2a6ab6f9f45..708edee7c29 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -29,6 +29,8 @@ import java.util.TreeSet; import java.util.regex.Pattern; import com.ibm.icu.impl.PluralRulesLoader; +import com.ibm.icu.number.FormattedNumber; +import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.util.Output; import com.ibm.icu.util.ULocale; @@ -2026,9 +2028,10 @@ public class PluralRules implements Serializable { public int hashCode() { return rules.hashCode(); } + /** - * Given a number, returns the keyword of the first rule that applies to - * the number. + * Given a floating-point number, returns the keyword of the first rule + * that applies to the number. * * @param number The number for which the rule has to be determined. * @return The keyword of the selected rule. @@ -2038,6 +2041,23 @@ public class PluralRules implements Serializable { return rules.select(new FixedDecimal(number)); } + /** + * Given a formatted number, returns the keyword of the first rule that + * applies to the number. + * + * A FormattedNumber allows you to specify an exponent or trailing zeros, + * which can affect the plural category. To get a FormattedNumber, see + * {@link NumberFormatter}. + * + * @param number The number for which the rule has to be determined. + * @return The keyword of the selected rule. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public String select(FormattedNumber number) { + return rules.select(number.getFixedDecimal()); + } + /** * Given a number, returns the keyword of the first rule that applies to * the number. diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java index 64d913825ba..c23b8a3cd35 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java @@ -41,6 +41,10 @@ import com.ibm.icu.dev.test.serializable.SerializableTestUtility; import com.ibm.icu.dev.util.CollectionUtilities; import com.ibm.icu.impl.Relation; import com.ibm.icu.impl.Utility; +import com.ibm.icu.number.FormattedNumber; +import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.Precision; +import com.ibm.icu.number.UnlocalizedNumberFormatter; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralRules; import com.ibm.icu.text.PluralRules.FixedDecimal; @@ -323,8 +327,8 @@ public class PluralRulesTest extends TestFmwk { public void testUniqueRules() { main: for (ULocale locale : factory.getAvailableULocales()) { PluralRules rules = factory.forLocale(locale); - Map keywordToRule = new HashMap(); - Collection samples = new LinkedHashSet(); + Map keywordToRule = new HashMap<>(); + Collection samples = new LinkedHashSet<>(); for (String keyword : rules.getKeywords()) { for (SampleType sampleType : SampleType.values()) { @@ -467,21 +471,59 @@ public class PluralRulesTest extends TestFmwk { @Test public void testBuiltInRules() { - // spot check - PluralRules rules = factory.forLocale(ULocale.US); - assertEquals("us 0", PluralRules.KEYWORD_OTHER, rules.select(0)); - assertEquals("us 1", PluralRules.KEYWORD_ONE, rules.select(1)); - assertEquals("us 2", PluralRules.KEYWORD_OTHER, rules.select(2)); - - rules = factory.forLocale(ULocale.JAPAN); - assertEquals("ja 0", PluralRules.KEYWORD_OTHER, rules.select(0)); - assertEquals("ja 1", PluralRules.KEYWORD_OTHER, rules.select(1)); - assertEquals("ja 2", PluralRules.KEYWORD_OTHER, rules.select(2)); - - rules = factory.forLocale(ULocale.createCanonical("ru")); - assertEquals("ru 0", PluralRules.KEYWORD_MANY, rules.select(0)); - assertEquals("ru 1", PluralRules.KEYWORD_ONE, rules.select(1)); - assertEquals("ru 2", PluralRules.KEYWORD_FEW, rules.select(2)); + Object[][] cases = { + {"en-US", PluralRules.KEYWORD_OTHER, 0}, + {"en-US", PluralRules.KEYWORD_ONE, 1}, + {"en-US", PluralRules.KEYWORD_OTHER, 2}, + {"ja-JP", PluralRules.KEYWORD_OTHER, 0}, + {"ja-JP", PluralRules.KEYWORD_OTHER, 1}, + {"ja-JP", PluralRules.KEYWORD_OTHER, 2}, + {"ru", PluralRules.KEYWORD_MANY, 0}, + {"ru", PluralRules.KEYWORD_ONE, 1}, + {"ru", PluralRules.KEYWORD_FEW, 2} + }; + for (Object[] cas : cases) { + ULocale locale = new ULocale((String) cas[0]); + PluralRules rules = factory.forLocale(locale); + String expectedKeyword = (String) cas[1]; + double number = (Integer) cas[2]; + String message = locale + " " + number; + // Check both as double and as FormattedNumber. + assertEquals(message, expectedKeyword, rules.select(number)); + FormattedNumber fn = NumberFormatter.withLocale(locale).format(number); + assertEquals(message, expectedKeyword, rules.select(fn)); + } + } + + @Test + public void testSelectTrailingZeros() { + UnlocalizedNumberFormatter unf = NumberFormatter.with() + .precision(Precision.fixedFraction(2)); + Object[][] cases = { + // 1) locale + // 2) double expected keyword + // 3) formatted number expected keyword (2 fraction digits) + // 4) input number + {"bs", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_OTHER, 5.2}, // 5.2 => two, but 5.20 => other + {"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 0.0}, + {"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 1.0}, + {"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_OTHER, 0.1}, // 0.1 => one, but 0.10 => other + {"si", PluralRules.KEYWORD_ONE, PluralRules.KEYWORD_ONE, 0.01}, // 0.01 => one + {"hsb", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_FEW, 1.03}, // (f % 100 == 3) => few + {"hsb", PluralRules.KEYWORD_FEW, PluralRules.KEYWORD_OTHER, 1.3}, // 1.3 => few, but 1.30 => other + }; + for (Object[] cas : cases) { + ULocale locale = new ULocale((String) cas[0]); + PluralRules rules = factory.forLocale(locale); + String expectedDoubleKeyword = (String) cas[1]; + String expectedFormattedKeyword = (String) cas[2]; + double number = (Double) cas[3]; + String message = locale + " " + number; + // Check both as double and as FormattedNumber. + assertEquals(message, expectedDoubleKeyword, rules.select(number)); + FormattedNumber fn = unf.locale(locale).format(number); + assertEquals(message, expectedFormattedKeyword, rules.select(fn)); + } } @Test @@ -605,7 +647,7 @@ public class PluralRulesTest extends TestFmwk { */ @Test public void TestGetSamples() { - Set uniqueRuleSet = new HashSet(); + Set uniqueRuleSet = new HashSet<>(); for (ULocale locale : factory.getAvailableULocales()) { uniqueRuleSet.add(PluralRules.getFunctionalEquivalent(locale, null)); } @@ -719,7 +761,7 @@ public class PluralRulesTest extends TestFmwk { } else if ("null".equals(valueList)) { values = null; } else { - values = new TreeSet(); + values = new TreeSet<>(); for (String value : valueList.split(",")) { values.add(Double.parseDouble(value)); } @@ -825,9 +867,9 @@ public class PluralRulesTest extends TestFmwk { // suppressed in // INTEGER but not // DECIMAL - }, { { "en", new HashSet(Arrays.asList(1.0d)) }, // check that 1 is suppressed + }, { { "en", new HashSet<>(Arrays.asList(1.0d)) }, // check that 1 is suppressed { "one", KeywordStatus.SUPPRESSED, null }, { "other", KeywordStatus.UNBOUNDED, null } }, }; - Output uniqueValue = new Output(); + Output uniqueValue = new Output<>(); for (Object[][] test : tests) { ULocale locale = new ULocale((String) test[0][0]); // NumberType numberType = (NumberType) test[1]; @@ -938,7 +980,7 @@ public class PluralRulesTest extends TestFmwk { }; private void generateLOCALE_SNAPSHOT() { - Comparator c = new CollectionUtilities.CollectionComparator(); + Comparator c = new CollectionUtilities.CollectionComparator<>(); Relation, PluralRules> setsToRules = Relation.of( new TreeMap, Set>(c), TreeSet.class, PLURAL_RULE_COMPARATOR); Relation data = Relation.of( @@ -1054,7 +1096,7 @@ public class PluralRulesTest extends TestFmwk { @Test public void TestSerialization() { - Output size = new Output(); + Output size = new Output<>(); int max = 0; for (ULocale locale : PluralRules.getAvailableULocales()) { PluralRules item = PluralRules.forLocale(locale); -- 2.40.0