diff --git a/icu4c/source/common/charstr.cpp b/icu4c/source/common/charstr.cpp index 0b785e9c010..852cc539457 100644 --- a/icu4c/source/common/charstr.cpp +++ b/icu4c/source/common/charstr.cpp @@ -126,15 +126,21 @@ char *CharString::getAppendBuffer(int32_t minCapacity, } CharString &CharString::appendInvariantChars(const UnicodeString &s, UErrorCode &errorCode) { + return appendInvariantChars(s.getBuffer(), s.length(), errorCode); +} + +CharString &CharString::appendInvariantChars(const UChar* uchars, int32_t ucharsLen, UErrorCode &errorCode) { if(U_FAILURE(errorCode)) { return *this; } - if (!uprv_isInvariantUnicodeString(s)) { + if (!uprv_isInvariantUString(uchars, ucharsLen)) { errorCode = U_INVARIANT_CONVERSION_ERROR; return *this; } - if(ensureCapacity(len+s.length()+1, 0, errorCode)) { - len+=s.extract(0, 0x7fffffff, buffer.getAlias()+len, buffer.getCapacity()-len, US_INV); + if(ensureCapacity(len+ucharsLen+1, 0, errorCode)) { + u_UCharsToChars(uchars, buffer.getAlias()+len, ucharsLen); + len += ucharsLen; + buffer[len] = 0; } return *this; } diff --git a/icu4c/source/common/charstr.h b/icu4c/source/common/charstr.h index 86f69c383a0..1a97e01988f 100644 --- a/icu4c/source/common/charstr.h +++ b/icu4c/source/common/charstr.h @@ -123,6 +123,7 @@ public: UErrorCode &errorCode); CharString &appendInvariantChars(const UnicodeString &s, UErrorCode &errorCode); + CharString &appendInvariantChars(const UChar* uchars, int32_t ucharsLen, UErrorCode& errorCode); /** * Appends a filename/path part, e.g., a directory name. diff --git a/icu4c/source/common/cmemory.h b/icu4c/source/common/cmemory.h index e3532c759e1..8f3b610de46 100644 --- a/icu4c/source/common/cmemory.h +++ b/icu4c/source/common/cmemory.h @@ -279,6 +279,10 @@ inline T *LocalMemory::allocateInsteadAndCopy(int32_t newCapacity, int32_t le * * Unlike LocalMemory and LocalArray, this class never adopts * (takes ownership of) another array. + * + * WARNING: MaybeStackArray only works with primitive (plain-old data) types. + * It does NOT know how to call a destructor! If you work with classes with + * destructors, consider LocalArray in localpointer.h. */ template class MaybeStackArray { diff --git a/icu4c/source/common/uinvchar.h b/icu4c/source/common/uinvchar.h index c4f9f88b9ad..56dddfa8fde 100644 --- a/icu4c/source/common/uinvchar.h +++ b/icu4c/source/common/uinvchar.h @@ -53,22 +53,6 @@ uprv_isInvariantString(const char *s, int32_t length); U_INTERNAL UBool U_EXPORT2 uprv_isInvariantUString(const UChar *s, int32_t length); -#ifdef __cplusplus - -/** - * Check if a UnicodeString only contains invariant characters. - * See utypes.h for details. - * - * @param s Input string. - * @return TRUE if s contains only invariant characters. - */ -U_INTERNAL inline UBool U_EXPORT2 -uprv_isInvariantUnicodeString(const icu::UnicodeString &s) { - return uprv_isInvariantUString(icu::toUCharPtr(s.getBuffer()), s.length()); -} - -#endif /* __cplusplus */ - /** * \def U_UPPER_ORDINAL * Get the ordinal number of an uppercase invariant character diff --git a/icu4c/source/data/build.xml b/icu4c/source/data/build.xml index efe0d3ce22f..20f7b5be2de 100644 --- a/icu4c/source/data/build.xml +++ b/icu4c/source/data/build.xml @@ -59,7 +59,7 @@ - + @@ -258,6 +258,18 @@ + + + + + + + + + + + + @@ -414,6 +426,9 @@ + + + diff --git a/icu4c/source/data/misc/miscfiles.mk b/icu4c/source/data/misc/miscfiles.mk index bd073c0f990..0c18119d47a 100644 --- a/icu4c/source/data/misc/miscfiles.mk +++ b/icu4c/source/data/misc/miscfiles.mk @@ -28,4 +28,4 @@ MISC_SOURCE = \ zoneinfo64.txt supplementalData.txt likelySubtags.txt plurals.txt \ numberingSystems.txt icuver.txt icustd.txt metadata.txt metaZones.txt \ windowsZones.txt keyTypeData.txt timezoneTypes.txt currencyNumericCodes.txt \ -genderList.txt dayPeriods.txt +genderList.txt dayPeriods.txt pluralRanges.txt diff --git a/icu4c/source/data/misc/pluralRanges.txt b/icu4c/source/data/misc/pluralRanges.txt new file mode 100644 index 00000000000..01d590f70f0 --- /dev/null +++ b/icu4c/source/data/misc/pluralRanges.txt @@ -0,0 +1,981 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +pluralRanges:table(nofallback){ + locales{ + af{"set05"} + ak{"set02"} + am{"set01"} + ar{"set18"} + as{"set01"} + az{"set04"} + be{"set15"} + bg{"set05"} + bn{"set01"} + bs{"set11"} + ca{"set05"} + cs{"set14"} + cy{"set17"} + da{"set06"} + de{"set04"} + el{"set04"} + en{"set05"} + es{"set05"} + et{"set05"} + eu{"set05"} + fa{"set02"} + fi{"set05"} + fil{"set06"} + fr{"set01"} + ga{"set16"} + gl{"set04"} + gsw{"set04"} + gu{"set01"} + he{"set13"} + hi{"set01"} + hr{"set11"} + hu{"set04"} + hy{"set01"} + id{"set00"} + io{"set05"} + is{"set06"} + it{"set04"} + ja{"set00"} + ka{"set03"} + kk{"set04"} + km{"set00"} + kn{"set01"} + ko{"set00"} + ky{"set04"} + lo{"set00"} + lt{"set15"} + lv{"set09"} + mk{"set08"} + ml{"set04"} + mn{"set04"} + mr{"set01"} + ms{"set00"} + my{"set00"} + nb{"set05"} + ne{"set04"} + nl{"set04"} + or{"set02"} + pa{"set06"} + pl{"set14"} + ps{"set01"} + pt{"set01"} + ro{"set10"} + ru{"set15"} + scn{"set04"} + sd{"set02"} + si{"set07"} + sk{"set14"} + sl{"set12"} + sq{"set04"} + sr{"set11"} + sv{"set05"} + sw{"set04"} + ta{"set04"} + te{"set04"} + th{"set00"} + tk{"set04"} + tr{"set04"} + ug{"set04"} + uk{"set15"} + ur{"set05"} + uz{"set04"} + vi{"set00"} + yue{"set00"} + zh{"set00"} + zu{"set01"} + } + rules{ + set00{ + { + "other", + "other", + "other", + } + } + set01{ + { + "one", + "one", + "one", + } + { + "one", + "other", + "other", + } + { + "other", + "other", + "other", + } + } + set02{ + { + "one", + "one", + "other", + } + { + "one", + "other", + "other", + } + { + "other", + "other", + "other", + } + } + set03{ + { + "one", + "other", + "one", + } + { + "other", + "one", + "other", + } + { + "other", + "other", + "other", + } + } + set04{ + { + "one", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "other", + "other", + } + } + set05{ + { + "one", + "other", + "other", + } + { + "other", + "one", + "other", + } + { + "other", + "other", + "other", + } + } + set06{ + { + "one", + "one", + "one", + } + { + "one", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "other", + "other", + } + } + set07{ + { + "one", + "one", + "one", + } + { + "one", + "other", + "other", + } + { + "other", + "one", + "other", + } + { + "other", + "other", + "other", + } + } + set08{ + { + "one", + "one", + "other", + } + { + "one", + "other", + "other", + } + { + "other", + "one", + "other", + } + { + "other", + "other", + "other", + } + } + set09{ + { + "zero", + "zero", + "other", + } + { + "zero", + "one", + "one", + } + { + "zero", + "other", + "other", + } + { + "one", + "zero", + "other", + } + { + "one", + "one", + "one", + } + { + "one", + "other", + "other", + } + { + "other", + "zero", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "other", + "other", + } + } + set10{ + { + "one", + "few", + "few", + } + { + "one", + "other", + "other", + } + { + "few", + "one", + "few", + } + { + "few", + "few", + "few", + } + { + "few", + "other", + "other", + } + { + "other", + "few", + "few", + } + { + "other", + "other", + "other", + } + } + set11{ + { + "one", + "one", + "one", + } + { + "one", + "few", + "few", + } + { + "one", + "other", + "other", + } + { + "few", + "one", + "one", + } + { + "few", + "few", + "few", + } + { + "few", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "few", + "few", + } + { + "other", + "other", + "other", + } + } + set12{ + { + "one", + "one", + "few", + } + { + "one", + "two", + "two", + } + { + "one", + "few", + "few", + } + { + "one", + "other", + "other", + } + { + "two", + "one", + "few", + } + { + "two", + "two", + "two", + } + { + "two", + "few", + "few", + } + { + "two", + "other", + "other", + } + { + "few", + "one", + "few", + } + { + "few", + "two", + "two", + } + { + "few", + "few", + "few", + } + { + "few", + "other", + "other", + } + { + "other", + "one", + "few", + } + { + "other", + "two", + "two", + } + { + "other", + "few", + "few", + } + { + "other", + "other", + "other", + } + } + set13{ + { + "one", + "two", + "other", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "two", + "many", + "other", + } + { + "two", + "other", + "other", + } + { + "many", + "many", + "many", + } + { + "many", + "other", + "many", + } + { + "other", + "one", + "other", + } + { + "other", + "two", + "other", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + set14{ + { + "one", + "few", + "few", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "few", + "few", + "few", + } + { + "few", + "many", + "many", + } + { + "few", + "other", + "other", + } + { + "many", + "one", + "one", + } + { + "many", + "few", + "few", + } + { + "many", + "many", + "many", + } + { + "many", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "few", + "few", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + set15{ + { + "one", + "one", + "one", + } + { + "one", + "few", + "few", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "few", + "one", + "one", + } + { + "few", + "few", + "few", + } + { + "few", + "many", + "many", + } + { + "few", + "other", + "other", + } + { + "many", + "one", + "one", + } + { + "many", + "few", + "few", + } + { + "many", + "many", + "many", + } + { + "many", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "few", + "few", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + set16{ + { + "one", + "two", + "two", + } + { + "one", + "few", + "few", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "two", + "few", + "few", + } + { + "two", + "many", + "many", + } + { + "two", + "other", + "other", + } + { + "few", + "few", + "few", + } + { + "few", + "many", + "many", + } + { + "few", + "other", + "other", + } + { + "many", + "many", + "many", + } + { + "many", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "two", + "two", + } + { + "other", + "few", + "few", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + set17{ + { + "zero", + "one", + "one", + } + { + "zero", + "two", + "two", + } + { + "zero", + "few", + "few", + } + { + "zero", + "many", + "many", + } + { + "zero", + "other", + "other", + } + { + "one", + "two", + "two", + } + { + "one", + "few", + "few", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "two", + "few", + "few", + } + { + "two", + "many", + "many", + } + { + "two", + "other", + "other", + } + { + "few", + "many", + "many", + } + { + "few", + "other", + "other", + } + { + "many", + "other", + "other", + } + { + "other", + "one", + "one", + } + { + "other", + "two", + "two", + } + { + "other", + "few", + "few", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + set18{ + { + "zero", + "one", + "zero", + } + { + "zero", + "two", + "zero", + } + { + "zero", + "few", + "few", + } + { + "zero", + "many", + "many", + } + { + "zero", + "other", + "other", + } + { + "one", + "two", + "other", + } + { + "one", + "few", + "few", + } + { + "one", + "many", + "many", + } + { + "one", + "other", + "other", + } + { + "two", + "few", + "few", + } + { + "two", + "many", + "many", + } + { + "two", + "other", + "other", + } + { + "few", + "few", + "few", + } + { + "few", + "many", + "many", + } + { + "few", + "other", + "other", + } + { + "many", + "few", + "few", + } + { + "many", + "many", + "many", + } + { + "many", + "other", + "other", + } + { + "other", + "one", + "other", + } + { + "other", + "two", + "other", + } + { + "other", + "few", + "few", + } + { + "other", + "many", + "many", + } + { + "other", + "other", + "other", + } + } + } +} diff --git a/icu4c/source/i18n/Makefile.in b/icu4c/source/i18n/Makefile.in index 98b36b4c917..fb5eb146bec 100644 --- a/icu4c/source/i18n/Makefile.in +++ b/icu4c/source/i18n/Makefile.in @@ -111,9 +111,9 @@ double-conversion-fast-dtoa.o double-conversion-strtod.o \ numparse_stringsegment.o numparse_parsednumber.o numparse_impl.o \ numparse_symbols.o numparse_decimal.o numparse_scientific.o numparse_currency.o \ numparse_affixes.o numparse_compositions.o numparse_validators.o \ +numrange_fluent.o numrange_impl.o \ erarules.o - ## Header files to install HEADERS = $(srcdir)/unicode/*.h diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index 18f48c6584e..fe1e71d905c 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -289,6 +289,8 @@ + + @@ -552,6 +554,7 @@ + diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters index db3470f83b2..c8f98451427 100644 --- a/icu4c/source/i18n/i18n.vcxproj.filters +++ b/icu4c/source/i18n/i18n.vcxproj.filters @@ -624,6 +624,12 @@ formatting + + formatting + + + formatting + formatting @@ -914,6 +920,9 @@ formatting + + formatting + formatting diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index e85c27766ab..bdc42e64a66 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -396,6 +396,8 @@ + + @@ -657,6 +659,7 @@ + diff --git a/icu4c/source/i18n/number_compact.cpp b/icu4c/source/i18n/number_compact.cpp index 40278e1a012..10942c35f53 100644 --- a/icu4c/source/i18n/number_compact.cpp +++ b/icu4c/source/i18n/number_compact.cpp @@ -273,13 +273,13 @@ void CompactHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micr if (U_FAILURE(status)) { return; } // Treat zero as if it had magnitude 0 - int magnitude; + int32_t magnitude; if (quantity.isZero()) { magnitude = 0; micros.rounder.apply(quantity, status); } else { // TODO: Revisit chooseMultiplierAndApply - int multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data, status); + int32_t multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data, status); magnitude = quantity.isZero() ? 0 : quantity.getMagnitude(); magnitude -= multiplier; } diff --git a/icu4c/source/i18n/number_decimalquantity.cpp b/icu4c/source/i18n/number_decimalquantity.cpp index 9d80e3349cb..2c4182b1c6e 100644 --- a/icu4c/source/i18n/number_decimalquantity.cpp +++ b/icu4c/source/i18n/number_decimalquantity.cpp @@ -1154,8 +1154,31 @@ const char16_t* DecimalQuantity::checkHealth() const { } bool DecimalQuantity::operator==(const DecimalQuantity& other) const { - // FIXME: Make a faster implementation. - return toString() == other.toString(); + bool basicEquals = + scale == other.scale + && precision == other.precision + && flags == other.flags + && lOptPos == other.lOptPos + && lReqPos == other.lReqPos + && rReqPos == other.rReqPos + && rOptPos == other.rOptPos + && isApproximate == other.isApproximate; + if (!basicEquals) { + return false; + } + + if (precision == 0) { + return true; + } else if (isApproximate) { + return origDouble == other.origDouble && origDelta == other.origDelta; + } else { + for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) { + if (getDigit(m) != other.getDigit(m)) { + return false; + } + } + return true; + } } UnicodeString DecimalQuantity::toString() const { diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index 06b10fd2765..e0e489a3573 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -363,6 +363,7 @@ UnlocalizedNumberFormatter::UnlocalizedNumberFormatter(const NFS& other) // No additional fields to assign } +// Make default copy constructor call the NumberFormatterSettings copy constructor. UnlocalizedNumberFormatter::UnlocalizedNumberFormatter(UNF&& src) U_NOEXCEPT : UNF(static_cast&&>(src)) {} @@ -383,6 +384,7 @@ UnlocalizedNumberFormatter& UnlocalizedNumberFormatter::operator=(UNF&& src) U_N return *this; } +// Make default copy constructor call the NumberFormatterSettings copy constructor. LocalizedNumberFormatter::LocalizedNumberFormatter(const LNF& other) : LNF(static_cast&>(other)) {} @@ -657,9 +659,9 @@ LocalizedNumberFormatter::formatDecimalQuantity(const DecimalQuantity& dq, UErro void LocalizedNumberFormatter::formatImpl(impl::UFormattedNumberData* results, UErrorCode& status) const { if (computeCompiled(status)) { - fCompiled->apply(results->quantity, results->string, status); + fCompiled->format(results->quantity, results->string, status); } else { - NumberFormatterImpl::applyStatic(fMacros, results->quantity, results->string, status); + NumberFormatterImpl::formatStatic(fMacros, results->quantity, results->string, status); } } @@ -706,7 +708,11 @@ bool LocalizedNumberFormatter::computeCompiled(UErrorCode& status) const { if (currentCount == fMacros.threshold && fMacros.threshold > 0) { // Build the data structure and then use it (slow to fast path). - const NumberFormatterImpl* compiled = NumberFormatterImpl::fromMacros(fMacros, status); + const NumberFormatterImpl* compiled = new NumberFormatterImpl(fMacros, status); + if (compiled == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return false; + } U_ASSERT(fCompiled == nullptr); const_cast(this)->fCompiled = compiled; umtx_storeRelease(*callCount, INT32_MIN); diff --git a/icu4c/source/i18n/number_formatimpl.cpp b/icu4c/source/i18n/number_formatimpl.cpp index 3f887128bcc..60c18ee284e 100644 --- a/icu4c/source/i18n/number_formatimpl.cpp +++ b/icu4c/source/i18n/number_formatimpl.cpp @@ -67,14 +67,18 @@ getCurrencyFormatInfo(const Locale& locale, const char* isoCode, UErrorCode& sta MicroPropsGenerator::~MicroPropsGenerator() = default; -NumberFormatterImpl* NumberFormatterImpl::fromMacros(const MacroProps& macros, UErrorCode& status) { - return new NumberFormatterImpl(macros, true, status); +NumberFormatterImpl::NumberFormatterImpl(const MacroProps& macros, UErrorCode& status) + : NumberFormatterImpl(macros, true, status) { } -void NumberFormatterImpl::applyStatic(const MacroProps& macros, DecimalQuantity& inValue, - NumberStringBuilder& outString, UErrorCode& status) { +int32_t NumberFormatterImpl::formatStatic(const MacroProps& macros, DecimalQuantity& inValue, + NumberStringBuilder& outString, UErrorCode& status) { NumberFormatterImpl impl(macros, false, status); - impl.applyUnsafe(inValue, outString, status); + MicroProps& micros = impl.preProcessUnsafe(inValue, status); + if (U_FAILURE(status)) { return 0; } + int32_t length = writeNumber(micros, inValue, outString, 0, status); + length += writeAffixes(micros, outString, 0, length, status); + return length; } int32_t NumberFormatterImpl::getPrefixSuffixStatic(const MacroProps& macros, int8_t signum, @@ -89,22 +93,40 @@ int32_t NumberFormatterImpl::getPrefixSuffixStatic(const MacroProps& macros, int // The "unsafe" method simply re-uses fMicros, eliminating the extra copy operation. // See MicroProps::processQuantity() for details. -void NumberFormatterImpl::apply(DecimalQuantity& inValue, NumberStringBuilder& outString, +int32_t NumberFormatterImpl::format(DecimalQuantity& inValue, NumberStringBuilder& outString, UErrorCode& status) const { - if (U_FAILURE(status)) { return; } MicroProps micros; - if (!fMicroPropsGenerator) { return; } - fMicroPropsGenerator->processQuantity(inValue, micros, status); - if (U_FAILURE(status)) { return; } - microsToString(micros, inValue, outString, status); + preProcess(inValue, micros, status); + if (U_FAILURE(status)) { return 0; } + int32_t length = writeNumber(micros, inValue, outString, 0, status); + length += writeAffixes(micros, outString, 0, length, status); + return length; } -void NumberFormatterImpl::applyUnsafe(DecimalQuantity& inValue, NumberStringBuilder& outString, - UErrorCode& status) { +void NumberFormatterImpl::preProcess(DecimalQuantity& inValue, MicroProps& microsOut, + UErrorCode& status) const { if (U_FAILURE(status)) { return; } + if (fMicroPropsGenerator == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return; + } + fMicroPropsGenerator->processQuantity(inValue, microsOut, status); + microsOut.rounder.apply(inValue, status); + microsOut.integerWidth.apply(inValue, status); +} + +MicroProps& NumberFormatterImpl::preProcessUnsafe(DecimalQuantity& inValue, UErrorCode& status) { + if (U_FAILURE(status)) { + return fMicros; // must always return a value + } + if (fMicroPropsGenerator == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return fMicros; // must always return a value + } fMicroPropsGenerator->processQuantity(inValue, fMicros, status); - if (U_FAILURE(status)) { return; } - microsToString(fMicros, inValue, outString, status); + fMicros.rounder.apply(inValue, status); + fMicros.integerWidth.apply(inValue, status); + return fMicros; } int32_t NumberFormatterImpl::getPrefixSuffix(int8_t signum, StandardPlural::Form plural, @@ -115,7 +137,7 @@ int32_t NumberFormatterImpl::getPrefixSuffix(int8_t signum, StandardPlural::Form const Modifier* modifier = fImmutablePatternModifier->getModifier(signum, plural); modifier->apply(outString, 0, 0, status); if (U_FAILURE(status)) { return 0; } - return modifier->getPrefixLength(status); + return modifier->getPrefixLength(); } int32_t NumberFormatterImpl::getPrefixSuffixUnsafe(int8_t signum, StandardPlural::Form plural, @@ -126,7 +148,7 @@ int32_t NumberFormatterImpl::getPrefixSuffixUnsafe(int8_t signum, StandardPlural fPatternModifier->setNumberProperties(signum, plural); fPatternModifier->apply(outString, 0, 0, status); if (U_FAILURE(status)) { return 0; } - return fPatternModifier->getPrefixLength(status); + return fPatternModifier->getPrefixLength(); } NumberFormatterImpl::NumberFormatterImpl(const MacroProps& macros, bool safe, UErrorCode& status) { @@ -344,25 +366,23 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe, // Outer modifier (CLDR units and currency long names) if (isCldrUnit) { fLongNameHandler.adoptInstead( - new LongNameHandler( - LongNameHandler::forMeasureUnit( - macros.locale, - macros.unit, - macros.perUnit, - unitWidth, - resolvePluralRules(macros.rules, macros.locale, status), - chain, - status))); + LongNameHandler::forMeasureUnit( + macros.locale, + macros.unit, + macros.perUnit, + unitWidth, + resolvePluralRules(macros.rules, macros.locale, status), + chain, + status)); chain = fLongNameHandler.getAlias(); } else if (isCurrency && unitWidth == UNUM_UNIT_WIDTH_FULL_NAME) { fLongNameHandler.adoptInstead( - new LongNameHandler( - LongNameHandler::forCurrencyLongNames( - macros.locale, - currency, - resolvePluralRules(macros.rules, macros.locale, status), - chain, - status))); + LongNameHandler::forCurrencyLongNames( + macros.locale, + currency, + resolvePluralRules(macros.rules, macros.locale, status), + chain, + status)); chain = fLongNameHandler.getAlias(); } else { // No outer modifier required @@ -404,50 +424,46 @@ NumberFormatterImpl::resolvePluralRules(const PluralRules* rulesPtr, const Local return fRules.getAlias(); } -int32_t NumberFormatterImpl::microsToString(const MicroProps& micros, DecimalQuantity& quantity, - NumberStringBuilder& string, UErrorCode& status) { - micros.rounder.apply(quantity, status); - micros.integerWidth.apply(quantity, status); - int32_t length = writeNumber(micros, quantity, string, status); - // NOTE: When range formatting is added, these modifiers can bubble up. - // For now, apply them all here at once. +int32_t NumberFormatterImpl::writeAffixes(const MicroProps& micros, NumberStringBuilder& string, + int32_t start, int32_t end, UErrorCode& status) { // Always apply the inner modifier (which is "strong"). - length += micros.modInner->apply(string, 0, length, status); + int32_t length = micros.modInner->apply(string, start, end, status); if (micros.padding.isValid()) { length += micros.padding - .padAndApply(*micros.modMiddle, *micros.modOuter, string, 0, length, status); + .padAndApply(*micros.modMiddle, *micros.modOuter, string, start, length + end, status); } else { - length += micros.modMiddle->apply(string, 0, length, status); - length += micros.modOuter->apply(string, 0, length, status); + length += micros.modMiddle->apply(string, start, length + end, status); + length += micros.modOuter->apply(string, start, length + end, status); } return length; } int32_t NumberFormatterImpl::writeNumber(const MicroProps& micros, DecimalQuantity& quantity, - NumberStringBuilder& string, UErrorCode& status) { + NumberStringBuilder& string, int32_t index, + UErrorCode& status) { int32_t length = 0; if (quantity.isInfinite()) { length += string.insert( - length, + length + index, micros.symbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kInfinitySymbol), UNUM_INTEGER_FIELD, status); } else if (quantity.isNaN()) { length += string.insert( - length, + length + index, micros.symbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kNaNSymbol), UNUM_INTEGER_FIELD, status); } else { // Add the integer digits - length += writeIntegerDigits(micros, quantity, string, status); + length += writeIntegerDigits(micros, quantity, string, length + index, status); // Add the decimal point if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == UNUM_DECIMAL_SEPARATOR_ALWAYS) { length += string.insert( - length, + length + index, micros.useCurrency ? micros.symbols->getSymbol( DecimalFormatSymbols::ENumberFormatSymbol::kMonetarySeparatorSymbol) : micros .symbols @@ -458,21 +474,22 @@ int32_t NumberFormatterImpl::writeNumber(const MicroProps& micros, DecimalQuanti } // Add the fraction digits - length += writeFractionDigits(micros, quantity, string, status); + length += writeFractionDigits(micros, quantity, string, length + index, status); } return length; } int32_t NumberFormatterImpl::writeIntegerDigits(const MicroProps& micros, DecimalQuantity& quantity, - NumberStringBuilder& string, UErrorCode& status) { + NumberStringBuilder& string, int32_t index, + UErrorCode& status) { int length = 0; int integerCount = quantity.getUpperDisplayMagnitude() + 1; for (int i = 0; i < integerCount; i++) { // Add grouping separator if (micros.grouping.groupAtPosition(i, quantity)) { length += string.insert( - 0, + index, micros.useCurrency ? micros.symbols->getSymbol( DecimalFormatSymbols::ENumberFormatSymbol::kMonetaryGroupingSeparatorSymbol) : micros.symbols->getSymbol( @@ -484,20 +501,21 @@ int32_t NumberFormatterImpl::writeIntegerDigits(const MicroProps& micros, Decima // Get and append the next digit value int8_t nextDigit = quantity.getDigit(i); length += utils::insertDigitFromSymbols( - string, 0, nextDigit, *micros.symbols, UNUM_INTEGER_FIELD, status); + string, index, nextDigit, *micros.symbols, UNUM_INTEGER_FIELD, status); } return length; } int32_t NumberFormatterImpl::writeFractionDigits(const MicroProps& micros, DecimalQuantity& quantity, - NumberStringBuilder& string, UErrorCode& status) { + NumberStringBuilder& string, int32_t index, + UErrorCode& status) { int length = 0; int fractionCount = -quantity.getLowerDisplayMagnitude(); for (int i = 0; i < fractionCount; i++) { // Get and append the next digit value int8_t nextDigit = quantity.getDigit(-i - 1); length += utils::insertDigitFromSymbols( - string, string.length(), nextDigit, *micros.symbols, UNUM_FRACTION_FIELD, status); + string, length + index, nextDigit, *micros.symbols, UNUM_FRACTION_FIELD, status); } return length; } diff --git a/icu4c/source/i18n/number_formatimpl.h b/icu4c/source/i18n/number_formatimpl.h index 744fecec13f..fda38c92845 100644 --- a/icu4c/source/i18n/number_formatimpl.h +++ b/icu4c/source/i18n/number_formatimpl.h @@ -29,14 +29,14 @@ class NumberFormatterImpl : public UMemory { * Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. * The caller owns the returned NumberFormatterImpl. */ - static NumberFormatterImpl *fromMacros(const MacroProps ¯os, UErrorCode &status); + NumberFormatterImpl(const MacroProps ¯os, UErrorCode &status); /** * Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once. */ - static void - applyStatic(const MacroProps ¯os, DecimalQuantity &inValue, NumberStringBuilder &outString, - UErrorCode &status); + static int32_t + formatStatic(const MacroProps ¯os, DecimalQuantity &inValue, NumberStringBuilder &outString, + UErrorCode &status); /** * Prints only the prefix and suffix; used for DecimalFormat getters. @@ -51,7 +51,12 @@ class NumberFormatterImpl : public UMemory { /** * Evaluates the "safe" MicroPropsGenerator created by "fromMacros". */ - void apply(DecimalQuantity& inValue, NumberStringBuilder& outString, UErrorCode& status) const; + int32_t format(DecimalQuantity& inValue, NumberStringBuilder& outString, UErrorCode& status) const; + + /** + * Like format(), but saves the result into an output MicroProps without additional processing. + */ + void preProcess(DecimalQuantity& inValue, MicroProps& microsOut, UErrorCode& status) const; /** * Like getPrefixSuffixStatic() but uses the safe compiled object. @@ -59,6 +64,19 @@ class NumberFormatterImpl : public UMemory { int32_t getPrefixSuffix(int8_t signum, StandardPlural::Form plural, NumberStringBuilder& outString, UErrorCode& status) const; + /** + * Synthesizes the output string from a MicroProps and DecimalQuantity. + * This method formats only the main number, not affixes. + */ + static int32_t writeNumber(const MicroProps& micros, DecimalQuantity& quantity, + NumberStringBuilder& string, int32_t index, UErrorCode& status); + + /** + * Adds the affixes. Intended to be called immediately after formatNumber. + */ + static int32_t writeAffixes(const MicroProps& micros, NumberStringBuilder& string, int32_t start, + int32_t end, UErrorCode& status); + private: // Head of the MicroPropsGenerator linked list: const MicroPropsGenerator *fMicroPropsGenerator = nullptr; @@ -85,7 +103,7 @@ class NumberFormatterImpl : public UMemory { NumberFormatterImpl(const MacroProps ¯os, bool safe, UErrorCode &status); - void applyUnsafe(DecimalQuantity &inValue, NumberStringBuilder &outString, UErrorCode &status); + MicroProps& preProcessUnsafe(DecimalQuantity &inValue, UErrorCode &status); int32_t getPrefixSuffixUnsafe(int8_t signum, StandardPlural::Form plural, NumberStringBuilder& outString, UErrorCode& status); @@ -113,31 +131,13 @@ class NumberFormatterImpl : public UMemory { const MicroPropsGenerator * macrosToMicroGenerator(const MacroProps ¯os, bool safe, UErrorCode &status); - /** - * Synthesizes the output string from a MicroProps and DecimalQuantity. - * - * @param micros - * The MicroProps after the quantity has been consumed. Will not be mutated. - * @param quantity - * The DecimalQuantity to be rendered. May be mutated. - * @param string - * The output string. Will be mutated. - */ - static int32_t - microsToString(const MicroProps µs, DecimalQuantity &quantity, NumberStringBuilder &string, - UErrorCode &status); - - static int32_t - writeNumber(const MicroProps µs, DecimalQuantity &quantity, NumberStringBuilder &string, - UErrorCode &status); - static int32_t writeIntegerDigits(const MicroProps µs, DecimalQuantity &quantity, NumberStringBuilder &string, - UErrorCode &status); + int32_t index, UErrorCode &status); static int32_t writeFractionDigits(const MicroProps µs, DecimalQuantity &quantity, NumberStringBuilder &string, - UErrorCode &status); + int32_t index, UErrorCode &status); }; } // namespace impl diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp index 26f9af4c9bd..fd8e8d381a1 100644 --- a/icu4c/source/i18n/number_longnames.cpp +++ b/icu4c/source/i18n/number_longnames.cpp @@ -39,7 +39,7 @@ static int32_t getIndex(const char* pluralKeyword, UErrorCode& status) { static UnicodeString getWithPlural( const UnicodeString* strings, - int32_t plural, + StandardPlural::Form plural, UErrorCode& status) { UnicodeString result = strings[plural]; if (result.isBogus()) { @@ -156,7 +156,7 @@ UnicodeString getPerUnitFormat(const Locale& locale, const UNumberUnitWidth &wid } // namespace -LongNameHandler +LongNameHandler* LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, const MeasureUnit &perUnit, const UNumberUnitWidth &width, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status) { @@ -173,20 +173,28 @@ LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, c } } - LongNameHandler result(rules, parent); + auto* result = new LongNameHandler(rules, parent); + if (result == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return nullptr; + } UnicodeString simpleFormats[ARRAY_LENGTH]; getMeasureData(loc, unit, width, simpleFormats, status); if (U_FAILURE(status)) { return result; } // TODO: What field to use for units? - simpleFormatsToModifiers(simpleFormats, UNUM_FIELD_COUNT, result.fModifiers, status); + result->simpleFormatsToModifiers(simpleFormats, UNUM_FIELD_COUNT, status); return result; } -LongNameHandler +LongNameHandler* LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, const UNumberUnitWidth &width, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status) { - LongNameHandler result(rules, parent); + auto* result = new LongNameHandler(rules, parent); + if (result == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return nullptr; + } UnicodeString primaryData[ARRAY_LENGTH]; getMeasureData(loc, unit, width, primaryData, status); if (U_FAILURE(status)) { return result; } @@ -213,46 +221,52 @@ LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, con if (U_FAILURE(status)) { return result; } } // TODO: What field to use for units? - multiSimpleFormatsToModifiers(primaryData, perUnitFormat, UNUM_FIELD_COUNT, result.fModifiers, status); + result->multiSimpleFormatsToModifiers(primaryData, perUnitFormat, UNUM_FIELD_COUNT, status); return result; } -LongNameHandler LongNameHandler::forCurrencyLongNames(const Locale &loc, const CurrencyUnit ¤cy, +LongNameHandler* LongNameHandler::forCurrencyLongNames(const Locale &loc, const CurrencyUnit ¤cy, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status) { - LongNameHandler result(rules, parent); + auto* result = new LongNameHandler(rules, parent); + if (result == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return nullptr; + } UnicodeString simpleFormats[ARRAY_LENGTH]; getCurrencyLongNameData(loc, currency, simpleFormats, status); - if (U_FAILURE(status)) { return result; } - simpleFormatsToModifiers(simpleFormats, UNUM_CURRENCY_FIELD, result.fModifiers, status); + if (U_FAILURE(status)) { return nullptr; } + result->simpleFormatsToModifiers(simpleFormats, UNUM_CURRENCY_FIELD, status); return result; } void LongNameHandler::simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, - SimpleModifier *output, UErrorCode &status) { + UErrorCode &status) { for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) { - UnicodeString simpleFormat = getWithPlural(simpleFormats, i, status); + StandardPlural::Form plural = static_cast(i); + UnicodeString simpleFormat = getWithPlural(simpleFormats, plural, status); if (U_FAILURE(status)) { return; } SimpleFormatter compiledFormatter(simpleFormat, 0, 1, status); if (U_FAILURE(status)) { return; } - output[i] = SimpleModifier(compiledFormatter, field, false); + fModifiers[i] = SimpleModifier(compiledFormatter, field, false, {this, 0, plural}); } } void LongNameHandler::multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat, - Field field, SimpleModifier *output, UErrorCode &status) { + Field field, UErrorCode &status) { SimpleFormatter trailCompiled(trailFormat, 1, 1, status); if (U_FAILURE(status)) { return; } for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) { - UnicodeString leadFormat = getWithPlural(leadFormats, i, status); + StandardPlural::Form plural = static_cast(i); + UnicodeString leadFormat = getWithPlural(leadFormats, plural, status); if (U_FAILURE(status)) { return; } UnicodeString compoundFormat; trailCompiled.format(leadFormat, compoundFormat, status); if (U_FAILURE(status)) { return; } SimpleFormatter compoundCompiled(compoundFormat, 0, 1, status); if (U_FAILURE(status)) { return; } - output[i] = SimpleModifier(compoundCompiled, field, false); + fModifiers[i] = SimpleModifier(compoundCompiled, field, false, {this, 0, plural}); } } @@ -265,4 +279,8 @@ void LongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps &mic micros.modOuter = &fModifiers[utils::getStandardPlural(rules, copy)]; } +const Modifier* LongNameHandler::getModifier(int8_t /*signum*/, StandardPlural::Form plural) const { + return &fModifiers[plural]; +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_longnames.h b/icu4c/source/i18n/number_longnames.h index 1d1e7dd3e86..a71d0caadf1 100644 --- a/icu4c/source/i18n/number_longnames.h +++ b/icu4c/source/i18n/number_longnames.h @@ -14,13 +14,13 @@ U_NAMESPACE_BEGIN namespace number { namespace impl { -class LongNameHandler : public MicroPropsGenerator, public UMemory { +class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public UMemory { public: - static LongNameHandler + static LongNameHandler* forCurrencyLongNames(const Locale &loc, const CurrencyUnit ¤cy, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status); - static LongNameHandler + static LongNameHandler* forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, const UNumberUnitWidth &width, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status); @@ -28,6 +28,8 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory { void processQuantity(DecimalQuantity &quantity, MicroProps µs, UErrorCode &status) const U_OVERRIDE; + const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const U_OVERRIDE; + private: SimpleModifier fModifiers[StandardPlural::Form::COUNT]; const PluralRules *rules; @@ -36,15 +38,14 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory { LongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent) : rules(rules), parent(parent) {} - static LongNameHandler + static LongNameHandler* forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit, const UNumberUnitWidth &width, const PluralRules *rules, const MicroPropsGenerator *parent, UErrorCode &status); - static void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, - SimpleModifier *output, UErrorCode &status); - static void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat, - Field field, SimpleModifier *output, UErrorCode &status); + void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, UErrorCode &status); + void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat, + Field field, UErrorCode &status); }; } // namespace impl diff --git a/icu4c/source/i18n/number_modifiers.cpp b/icu4c/source/i18n/number_modifiers.cpp index 4385499b54f..d92ec63b08d 100644 --- a/icu4c/source/i18n/number_modifiers.cpp +++ b/icu4c/source/i18n/number_modifiers.cpp @@ -53,6 +53,21 @@ void U_CALLCONV initDefaultCurrencySpacing(UErrorCode &status) { Modifier::~Modifier() = default; +Modifier::Parameters::Parameters() + : obj(nullptr) {} + +Modifier::Parameters::Parameters( + const ModifierStore* _obj, int8_t _signum, StandardPlural::Form _plural) + : obj(_obj), signum(_signum), plural(_plural) {} + +ModifierStore::~ModifierStore() = default; + +AdoptingModifierStore::~AdoptingModifierStore() { + for (const Modifier *mod : mods) { + delete mod; + } +} + int32_t ConstantAffixModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex, UErrorCode &status) const { @@ -62,13 +77,11 @@ int32_t ConstantAffixModifier::apply(NumberStringBuilder &output, int leftIndex, return length; } -int32_t ConstantAffixModifier::getPrefixLength(UErrorCode &status) const { - (void)status; +int32_t ConstantAffixModifier::getPrefixLength() const { return fPrefix.length(); } -int32_t ConstantAffixModifier::getCodePointCount(UErrorCode &status) const { - (void)status; +int32_t ConstantAffixModifier::getCodePointCount() const { return fPrefix.countChar32() + fSuffix.countChar32(); } @@ -76,8 +89,38 @@ bool ConstantAffixModifier::isStrong() const { return fStrong; } +bool ConstantAffixModifier::containsField(UNumberFormatFields field) const { + (void)field; + // This method is not currently used. + U_ASSERT(false); + return false; +} + +void ConstantAffixModifier::getParameters(Parameters& output) const { + (void)output; + // This method is not currently used. + U_ASSERT(false); +} + +bool ConstantAffixModifier::semanticallyEquivalent(const Modifier& other) const { + auto* _other = dynamic_cast(&other); + if (_other == nullptr) { + return false; + } + return fPrefix == _other->fPrefix + && fSuffix == _other->fSuffix + && fField == _other->fField + && fStrong == _other->fStrong; +} + + SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong) - : fCompiledPattern(simpleFormatter.compiledPattern), fField(field), fStrong(strong) { + : SimpleModifier(simpleFormatter, field, strong, {}) {} + +SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong, + const Modifier::Parameters parameters) + : fCompiledPattern(simpleFormatter.compiledPattern), fField(field), fStrong(strong), + fParameters(parameters) { int32_t argLimit = SimpleFormatter::getArgumentLimit( fCompiledPattern.getBuffer(), fCompiledPattern.length()); if (argLimit == 0) { @@ -90,15 +133,19 @@ SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field fie } else { U_ASSERT(argLimit == 1); if (fCompiledPattern.charAt(1) != 0) { + // Found prefix fPrefixLength = fCompiledPattern.charAt(1) - ARG_NUM_LIMIT; fSuffixOffset = 3 + fPrefixLength; } else { + // No prefix fPrefixLength = 0; fSuffixOffset = 2; } if (3 + fPrefixLength < fCompiledPattern.length()) { + // Found suffix fSuffixLength = fCompiledPattern.charAt(fSuffixOffset) - ARG_NUM_LIMIT; } else { + // No suffix fSuffixLength = 0; } } @@ -113,13 +160,11 @@ int32_t SimpleModifier::apply(NumberStringBuilder &output, int leftIndex, int ri return formatAsPrefixSuffix(output, leftIndex, rightIndex, fField, status); } -int32_t SimpleModifier::getPrefixLength(UErrorCode &status) const { - (void)status; +int32_t SimpleModifier::getPrefixLength() const { return fPrefixLength; } -int32_t SimpleModifier::getCodePointCount(UErrorCode &status) const { - (void)status; +int32_t SimpleModifier::getCodePointCount() const { int32_t count = 0; if (fPrefixLength > 0) { count += fCompiledPattern.countChar32(2, fPrefixLength); @@ -134,10 +179,35 @@ bool SimpleModifier::isStrong() const { return fStrong; } +bool SimpleModifier::containsField(UNumberFormatFields field) const { + (void)field; + // This method is not currently used. + U_ASSERT(false); + return false; +} + +void SimpleModifier::getParameters(Parameters& output) const { + output = fParameters; +} + +bool SimpleModifier::semanticallyEquivalent(const Modifier& other) const { + auto* _other = dynamic_cast(&other); + if (_other == nullptr) { + return false; + } + if (fParameters.obj != nullptr) { + return fParameters.obj == _other->fParameters.obj; + } + return fCompiledPattern == _other->fCompiledPattern + && fField == _other->fField + && fStrong == _other->fStrong; +} + + int32_t SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startIndex, int32_t endIndex, Field field, UErrorCode &status) const { - if (fSuffixOffset == -1) { + if (fSuffixOffset == -1 && fPrefixLength + fSuffixLength > 0) { // There is no argument for the inner number; overwrite the entire segment with our string. return result.splice(startIndex, endIndex, fCompiledPattern, 2, 2 + fPrefixLength, field, status); } else { @@ -157,6 +227,65 @@ SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startI } } + +int32_t +SimpleModifier::formatTwoArgPattern(const SimpleFormatter& compiled, NumberStringBuilder& result, + int32_t index, int32_t* outPrefixLength, int32_t* outSuffixLength, + Field field, UErrorCode& status) { + const UnicodeString& compiledPattern = compiled.compiledPattern; + int32_t argLimit = SimpleFormatter::getArgumentLimit( + compiledPattern.getBuffer(), compiledPattern.length()); + if (argLimit != 2) { + status = U_INTERNAL_PROGRAM_ERROR; + return 0; + } + int32_t offset = 1; // offset into compiledPattern + int32_t length = 0; // chars added to result + + int32_t prefixLength = compiledPattern.charAt(offset); + offset++; + if (prefixLength < ARG_NUM_LIMIT) { + // No prefix + prefixLength = 0; + } else { + prefixLength -= ARG_NUM_LIMIT; + result.insert(index + length, compiledPattern, offset, offset + prefixLength, field, status); + offset += prefixLength; + length += prefixLength; + offset++; + } + + int32_t infixLength = compiledPattern.charAt(offset); + offset++; + if (infixLength < ARG_NUM_LIMIT) { + // No infix + infixLength = 0; + } else { + infixLength -= ARG_NUM_LIMIT; + result.insert(index + length, compiledPattern, offset, offset + infixLength, field, status); + offset += infixLength; + length += infixLength; + offset++; + } + + int32_t suffixLength; + if (offset == compiledPattern.length()) { + // No suffix + suffixLength = 0; + } else { + suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT; + offset++; + result.insert(index + length, compiledPattern, offset, offset + suffixLength, field, status); + length += suffixLength; + } + + *outPrefixLength = prefixLength; + *outSuffixLength = suffixLength; + + return length; +} + + int32_t ConstantMultiFieldModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex, UErrorCode &status) const { int32_t length = output.insert(leftIndex, fPrefix, status); @@ -171,13 +300,11 @@ int32_t ConstantMultiFieldModifier::apply(NumberStringBuilder &output, int leftI return length; } -int32_t ConstantMultiFieldModifier::getPrefixLength(UErrorCode &status) const { - (void)status; +int32_t ConstantMultiFieldModifier::getPrefixLength() const { return fPrefix.length(); } -int32_t ConstantMultiFieldModifier::getCodePointCount(UErrorCode &status) const { - (void)status; +int32_t ConstantMultiFieldModifier::getCodePointCount() const { return fPrefix.codePointCount() + fSuffix.codePointCount(); } @@ -185,6 +312,29 @@ bool ConstantMultiFieldModifier::isStrong() const { return fStrong; } +bool ConstantMultiFieldModifier::containsField(UNumberFormatFields field) const { + return fPrefix.containsField(field) || fSuffix.containsField(field); +} + +void ConstantMultiFieldModifier::getParameters(Parameters& output) const { + output = fParameters; +} + +bool ConstantMultiFieldModifier::semanticallyEquivalent(const Modifier& other) const { + auto* _other = dynamic_cast(&other); + if (_other == nullptr) { + return false; + } + if (fParameters.obj != nullptr) { + return fParameters.obj == _other->fParameters.obj; + } + return fPrefix.contentEquals(_other->fPrefix) + && fSuffix.contentEquals(_other->fSuffix) + && fOverwrite == _other->fOverwrite + && fStrong == _other->fStrong; +} + + CurrencySpacingEnabledModifier::CurrencySpacingEnabledModifier(const NumberStringBuilder &prefix, const NumberStringBuilder &suffix, bool overwrite, diff --git a/icu4c/source/i18n/number_modifiers.h b/icu4c/source/i18n/number_modifiers.h index a553100cd92..65ada937d03 100644 --- a/icu4c/source/i18n/number_modifiers.h +++ b/icu4c/source/i18n/number_modifiers.h @@ -31,12 +31,18 @@ class U_I18N_API ConstantAffixModifier : public Modifier, public UObject { int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE; + int32_t getPrefixLength() const U_OVERRIDE; - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE; + int32_t getCodePointCount() const U_OVERRIDE; bool isStrong() const U_OVERRIDE; + bool containsField(UNumberFormatFields field) const U_OVERRIDE; + + void getParameters(Parameters& output) const U_OVERRIDE; + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE; + private: UnicodeString fPrefix; UnicodeString fSuffix; @@ -52,21 +58,30 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory { public: SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong); + SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong, + const Modifier::Parameters parameters); + // Default constructor for LongNameHandler.h SimpleModifier(); int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE; + int32_t getPrefixLength() const U_OVERRIDE; - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE; + int32_t getCodePointCount() const U_OVERRIDE; bool isStrong() const U_OVERRIDE; + bool containsField(UNumberFormatFields field) const U_OVERRIDE; + + void getParameters(Parameters& output) const U_OVERRIDE; + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE; + /** * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because - * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not depend on it. + * NumberStringBuilder is an internal class and SimpleFormatterImpl feels like it should not depend on it. * *

* Formats a value that is already stored inside the StringBuilder result between the indices @@ -85,16 +100,33 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory { * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. */ int32_t - formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startIndex, int32_t endIndex, Field field, - UErrorCode &status) const; + formatAsPrefixSuffix(NumberStringBuilder& result, int32_t startIndex, int32_t endIndex, Field field, + UErrorCode& status) const; + + /** + * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code. + * I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other. + * + *

+ * Applies the compiled two-argument pattern to the NumberStringBuilder. + * + *

+ * This method is optimized for the case where the prefix and suffix are often empty, such as + * in the range pattern like "{0}-{1}". + */ + static int32_t + formatTwoArgPattern(const SimpleFormatter& compiled, NumberStringBuilder& result, + int32_t index, int32_t* outPrefixLength, int32_t* outSuffixLength, + Field field, UErrorCode& status); private: UnicodeString fCompiledPattern; Field fField; - bool fStrong; - int32_t fPrefixLength; - int32_t fSuffixOffset; - int32_t fSuffixLength; + bool fStrong = false; + int32_t fPrefixLength = 0; + int32_t fSuffixOffset = -1; + int32_t fSuffixLength = 0; + Modifier::Parameters fParameters; }; /** @@ -103,6 +135,18 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory { */ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { public: + ConstantMultiFieldModifier( + const NumberStringBuilder &prefix, + const NumberStringBuilder &suffix, + bool overwrite, + bool strong, + const Modifier::Parameters parameters) + : fPrefix(prefix), + fSuffix(suffix), + fOverwrite(overwrite), + fStrong(strong), + fParameters(parameters) {} + ConstantMultiFieldModifier( const NumberStringBuilder &prefix, const NumberStringBuilder &suffix, @@ -116,12 +160,18 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE; + int32_t getPrefixLength() const U_OVERRIDE; - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE; + int32_t getCodePointCount() const U_OVERRIDE; bool isStrong() const U_OVERRIDE; + bool containsField(UNumberFormatFields field) const U_OVERRIDE; + + void getParameters(Parameters& output) const U_OVERRIDE; + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE; + protected: // NOTE: In Java, these are stored as array pointers. In C++, the NumberStringBuilder is stored by // value and is treated internally as immutable. @@ -129,6 +179,7 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory { NumberStringBuilder fSuffix; bool fOverwrite; bool fStrong; + Modifier::Parameters fParameters; }; /** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */ @@ -192,13 +243,11 @@ class U_I18N_API EmptyModifier : public Modifier, public UMemory { return 0; } - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE { - (void)status; + int32_t getPrefixLength() const U_OVERRIDE { return 0; } - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE { - (void)status; + int32_t getCodePointCount() const U_OVERRIDE { return 0; } @@ -206,55 +255,75 @@ class U_I18N_API EmptyModifier : public Modifier, public UMemory { return fStrong; } + bool containsField(UNumberFormatFields field) const U_OVERRIDE { + (void)field; + return false; + } + + void getParameters(Parameters& output) const U_OVERRIDE { + output.obj = nullptr; + } + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE { + return other.getCodePointCount() == 0; + } + private: bool fStrong; }; /** - * A ParameterizedModifier by itself is NOT a Modifier. Rather, it wraps a data structure containing two or more - * Modifiers and returns the modifier appropriate for the current situation. + * This implementation of ModifierStore adopts Modifer pointers. */ -class U_I18N_API ParameterizedModifier : public UMemory { +class U_I18N_API AdoptingModifierStore : public ModifierStore, public UMemory { public: - // NOTE: mods is zero-initialized (to nullptr) - ParameterizedModifier() : mods() { - } + virtual ~AdoptingModifierStore(); + + static constexpr StandardPlural::Form DEFAULT_STANDARD_PLURAL = StandardPlural::OTHER; + + AdoptingModifierStore() = default; // No copying! - ParameterizedModifier(const ParameterizedModifier &other) = delete; + AdoptingModifierStore(const AdoptingModifierStore &other) = delete; - ~ParameterizedModifier() { - for (const Modifier *mod : mods) { - delete mod; - } - } - - void adoptPositiveNegativeModifiers( - const Modifier *positive, const Modifier *zero, const Modifier *negative) { - mods[2] = positive; - mods[1] = zero; - mods[0] = negative; - } - - /** The modifier is ADOPTED. */ - void adoptSignPluralModifier(int8_t signum, StandardPlural::Form plural, const Modifier *mod) { + /** + * Sets the Modifier with the specified signum and plural form. + */ + void adoptModifier(int8_t signum, StandardPlural::Form plural, const Modifier *mod) { + U_ASSERT(mods[getModIndex(signum, plural)] == nullptr); mods[getModIndex(signum, plural)] = mod; } - /** Returns a reference to the modifier; no ownership change. */ - const Modifier *getModifier(int8_t signum) const { - return mods[signum + 1]; + /** + * Sets the Modifier with the specified signum. + * The modifier will apply to all plural forms. + */ + void adoptModifierWithoutPlural(int8_t signum, const Modifier *mod) { + U_ASSERT(mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)] == nullptr); + mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)] = mod; } /** Returns a reference to the modifier; no ownership change. */ - const Modifier *getModifier(int8_t signum, StandardPlural::Form plural) const { - return mods[getModIndex(signum, plural)]; + const Modifier *getModifier(int8_t signum, StandardPlural::Form plural) const U_OVERRIDE { + const Modifier* modifier = mods[getModIndex(signum, plural)]; + if (modifier == nullptr && plural != DEFAULT_STANDARD_PLURAL) { + modifier = mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)]; + } + return modifier; + } + + /** Returns a reference to the modifier; no ownership change. */ + const Modifier *getModifierWithoutPlural(int8_t signum) const { + return mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)]; } private: - const Modifier *mods[3 * StandardPlural::COUNT]; + // NOTE: mods is zero-initialized (to nullptr) + const Modifier *mods[3 * StandardPlural::COUNT] = {}; inline static int32_t getModIndex(int8_t signum, StandardPlural::Form plural) { + U_ASSERT(signum >= -1 && signum <= 1); + U_ASSERT(plural >= 0 && plural < StandardPlural::COUNT); return static_cast(plural) * 3 + (signum + 1); } }; diff --git a/icu4c/source/i18n/number_padding.cpp b/icu4c/source/i18n/number_padding.cpp index 97e7b6014f9..31684d7208b 100644 --- a/icu4c/source/i18n/number_padding.cpp +++ b/icu4c/source/i18n/number_padding.cpp @@ -62,7 +62,7 @@ Padder Padder::forProperties(const DecimalFormatProperties& properties) { int32_t Padder::padAndApply(const Modifier &mod1, const Modifier &mod2, NumberStringBuilder &string, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const { - int32_t modLength = mod1.getCodePointCount(status) + mod2.getCodePointCount(status); + int32_t modLength = mod1.getCodePointCount() + mod2.getCodePointCount(); int32_t requiredPadding = fWidth - modLength - string.codePointCount(); U_ASSERT(leftIndex == 0 && rightIndex == string.length()); // fix the previous line to remove this assertion diff --git a/icu4c/source/i18n/number_patternmodifier.cpp b/icu4c/source/i18n/number_patternmodifier.cpp index 09f4f392182..4c61a0d35bc 100644 --- a/icu4c/source/i18n/number_patternmodifier.cpp +++ b/icu4c/source/i18n/number_patternmodifier.cpp @@ -69,7 +69,7 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren StandardPlural::Form::MANY, StandardPlural::Form::OTHER}; - auto pm = new ParameterizedModifier(); + auto pm = new AdoptingModifierStore(); if (pm == nullptr) { status = U_MEMORY_ALLOCATION_ERROR; return nullptr; @@ -79,11 +79,11 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren // Slower path when we require the plural keyword. for (StandardPlural::Form plural : STANDARD_PLURAL_VALUES) { setNumberProperties(1, plural); - pm->adoptSignPluralModifier(1, plural, createConstantModifier(status)); + pm->adoptModifier(1, plural, createConstantModifier(status)); setNumberProperties(0, plural); - pm->adoptSignPluralModifier(0, plural, createConstantModifier(status)); + pm->adoptModifier(0, plural, createConstantModifier(status)); setNumberProperties(-1, plural); - pm->adoptSignPluralModifier(-1, plural, createConstantModifier(status)); + pm->adoptModifier(-1, plural, createConstantModifier(status)); } if (U_FAILURE(status)) { delete pm; @@ -93,12 +93,11 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren } else { // Faster path when plural keyword is not needed. setNumberProperties(1, StandardPlural::Form::COUNT); - Modifier* positive = createConstantModifier(status); + pm->adoptModifierWithoutPlural(1, createConstantModifier(status)); setNumberProperties(0, StandardPlural::Form::COUNT); - Modifier* zero = createConstantModifier(status); + pm->adoptModifierWithoutPlural(0, createConstantModifier(status)); setNumberProperties(-1, StandardPlural::Form::COUNT); - Modifier* negative = createConstantModifier(status); - pm->adoptPositiveNegativeModifiers(positive, zero, negative); + pm->adoptModifierWithoutPlural(-1, createConstantModifier(status)); if (U_FAILURE(status)) { delete pm; return nullptr; @@ -120,7 +119,7 @@ ConstantMultiFieldModifier* MutablePatternModifier::createConstantModifier(UErro } } -ImmutablePatternModifier::ImmutablePatternModifier(ParameterizedModifier* pm, const PluralRules* rules, +ImmutablePatternModifier::ImmutablePatternModifier(AdoptingModifierStore* pm, const PluralRules* rules, const MicroPropsGenerator* parent) : pm(pm), rules(rules), parent(parent) {} @@ -132,7 +131,7 @@ void ImmutablePatternModifier::processQuantity(DecimalQuantity& quantity, MicroP void ImmutablePatternModifier::applyToMicros(MicroProps& micros, DecimalQuantity& quantity) const { if (rules == nullptr) { - micros.modMiddle = pm->getModifier(quantity.signum()); + micros.modMiddle = pm->getModifierWithoutPlural(quantity.signum()); } else { // TODO: Fix this. Avoid the copy. DecimalQuantity copy(quantity); @@ -144,7 +143,7 @@ void ImmutablePatternModifier::applyToMicros(MicroProps& micros, DecimalQuantity const Modifier* ImmutablePatternModifier::getModifier(int8_t signum, StandardPlural::Form plural) const { if (rules == nullptr) { - return pm->getModifier(signum); + return pm->getModifierWithoutPlural(signum); } else { return pm->getModifier(signum, plural); } @@ -204,23 +203,25 @@ int32_t MutablePatternModifier::apply(NumberStringBuilder& output, int32_t leftI return prefixLen + overwriteLen + suffixLen; } -int32_t MutablePatternModifier::getPrefixLength(UErrorCode& status) const { +int32_t MutablePatternModifier::getPrefixLength() const { // The unsafe code path performs self-mutation, so we need a const_cast. // This method needs to be const because it overrides a const method in the parent class. auto nonConstThis = const_cast(this); // Enter and exit CharSequence Mode to get the length. + UErrorCode status = U_ZERO_ERROR; // status fails only with an iilegal argument exception nonConstThis->prepareAffix(true); int result = AffixUtils::unescapedCodePointCount(currentAffix, *this, status); // prefix length return result; } -int32_t MutablePatternModifier::getCodePointCount(UErrorCode& status) const { +int32_t MutablePatternModifier::getCodePointCount() const { // The unsafe code path performs self-mutation, so we need a const_cast. // This method needs to be const because it overrides a const method in the parent class. auto nonConstThis = const_cast(this); // Render the affixes to get the length + UErrorCode status = U_ZERO_ERROR; // status fails only with an iilegal argument exception nonConstThis->prepareAffix(true); int result = AffixUtils::unescapedCodePointCount(currentAffix, *this, status); // prefix length nonConstThis->prepareAffix(false); @@ -232,6 +233,26 @@ bool MutablePatternModifier::isStrong() const { return fStrong; } +bool MutablePatternModifier::containsField(UNumberFormatFields field) const { + (void)field; + // This method is not currently used. + U_ASSERT(false); + return false; +} + +void MutablePatternModifier::getParameters(Parameters& output) const { + (void)output; + // This method is not currently used. + U_ASSERT(false); +} + +bool MutablePatternModifier::semanticallyEquivalent(const Modifier& other) const { + (void)other; + // This method is not currently used. + U_ASSERT(false); + return false; +} + int32_t MutablePatternModifier::insertPrefix(NumberStringBuilder& sb, int position, UErrorCode& status) { prepareAffix(true); int length = AffixUtils::unescape(currentAffix, sb, position, *this, status); diff --git a/icu4c/source/i18n/number_patternmodifier.h b/icu4c/source/i18n/number_patternmodifier.h index 2854ca056a1..ea80d6305e7 100644 --- a/icu4c/source/i18n/number_patternmodifier.h +++ b/icu4c/source/i18n/number_patternmodifier.h @@ -18,13 +18,13 @@ U_NAMESPACE_BEGIN // Export an explicit template instantiation of the LocalPointer that is used as a -// data member of ParameterizedModifier. +// data member of AdoptingModifierStore. // (When building DLLs for Windows this is required.) #if U_PF_WINDOWS <= U_PLATFORM && U_PLATFORM <= U_PF_CYGWIN // Ignore warning 4661 as LocalPointerBase does not use operator== or operator!= #pragma warning(suppress: 4661) -template class U_I18N_API LocalPointerBase; -template class U_I18N_API LocalPointer; +template class U_I18N_API LocalPointerBase; +template class U_I18N_API LocalPointer; #endif namespace number { @@ -45,10 +45,10 @@ class U_I18N_API ImmutablePatternModifier : public MicroPropsGenerator, public U const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const; private: - ImmutablePatternModifier(ParameterizedModifier* pm, const PluralRules* rules, + ImmutablePatternModifier(AdoptingModifierStore* pm, const PluralRules* rules, const MicroPropsGenerator* parent); - const LocalPointer pm; + const LocalPointer pm; const PluralRules* rules; const MicroPropsGenerator* parent; @@ -178,12 +178,18 @@ class U_I18N_API MutablePatternModifier int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE; + int32_t getPrefixLength() const U_OVERRIDE; - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE; + int32_t getCodePointCount() const U_OVERRIDE; bool isStrong() const U_OVERRIDE; + bool containsField(UNumberFormatFields field) const U_OVERRIDE; + + void getParameters(Parameters& output) const U_OVERRIDE; + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE; + /** * Returns the string that substitutes a given symbol type in a pattern. */ diff --git a/icu4c/source/i18n/number_scientific.cpp b/icu4c/source/i18n/number_scientific.cpp index e9adfb402bd..009e4dfa5b5 100644 --- a/icu4c/source/i18n/number_scientific.cpp +++ b/icu4c/source/i18n/number_scientific.cpp @@ -76,17 +76,16 @@ int32_t ScientificModifier::apply(NumberStringBuilder &output, int32_t /*leftInd return i - rightIndex; } -int32_t ScientificModifier::getPrefixLength(UErrorCode &status) const { - (void)status; +int32_t ScientificModifier::getPrefixLength() const { // TODO: Localized exponent separator location. return 0; } -int32_t ScientificModifier::getCodePointCount(UErrorCode &status) const { - (void)status; - // This method is not used for strong modifiers. - U_ASSERT(false); - return 0; +int32_t ScientificModifier::getCodePointCount() const { + // NOTE: This method is only called one place, NumberRangeFormatterImpl. + // The call site only cares about != 0 and != 1. + // Return a very large value so that if this method is used elsewhere, we should notice. + return 999; } bool ScientificModifier::isStrong() const { @@ -94,6 +93,27 @@ bool ScientificModifier::isStrong() const { return true; } +bool ScientificModifier::containsField(UNumberFormatFields field) const { + (void)field; + // This method is not used for inner modifiers. + U_ASSERT(false); + return false; +} + +void ScientificModifier::getParameters(Parameters& output) const { + // Not part of any plural sets + output.obj = nullptr; +} + +bool ScientificModifier::semanticallyEquivalent(const Modifier& other) const { + auto* _other = dynamic_cast(&other); + if (_other == nullptr) { + return false; + } + // TODO: Check for locale symbols and settings as well? Could be less efficient. + return fExponent == _other->fExponent; +} + // Note: Visual Studio does not compile this function without full name space. Why? icu::number::impl::ScientificHandler::ScientificHandler(const Notation *notation, const DecimalFormatSymbols *symbols, const MicroPropsGenerator *parent) : diff --git a/icu4c/source/i18n/number_scientific.h b/icu4c/source/i18n/number_scientific.h index 974ab3adb61..e377bd941ef 100644 --- a/icu4c/source/i18n/number_scientific.h +++ b/icu4c/source/i18n/number_scientific.h @@ -24,12 +24,18 @@ class U_I18N_API ScientificModifier : public UMemory, public Modifier { int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex, UErrorCode &status) const U_OVERRIDE; - int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE; + int32_t getPrefixLength() const U_OVERRIDE; - int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE; + int32_t getCodePointCount() const U_OVERRIDE; bool isStrong() const U_OVERRIDE; + bool containsField(UNumberFormatFields field) const U_OVERRIDE; + + void getParameters(Parameters& output) const U_OVERRIDE; + + bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE; + private: int32_t fExponent; const ScientificHandler *fHandler; diff --git a/icu4c/source/i18n/number_stringbuilder.cpp b/icu4c/source/i18n/number_stringbuilder.cpp index 37770d11d51..74ba33fbbc1 100644 --- a/icu4c/source/i18n/number_stringbuilder.cpp +++ b/icu4c/source/i18n/number_stringbuilder.cpp @@ -241,6 +241,9 @@ NumberStringBuilder::insert(int32_t index, const NumberStringBuilder &other, UEr } int32_t NumberStringBuilder::prepareForInsert(int32_t index, int32_t count, UErrorCode &status) { + U_ASSERT(index >= 0); + U_ASSERT(index <= fLength); + U_ASSERT(count >= 0); if (index == 0 && fZero - count >= 0) { // Append to start fZero -= count; @@ -485,4 +488,13 @@ void NumberStringBuilder::getAllFieldPositions(FieldPositionIteratorHandler& fpi } } +bool NumberStringBuilder::containsField(Field field) const { + for (int32_t i = 0; i < fLength; i++) { + if (field == fieldAt(i)) { + return true; + } + } + return false; +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_stringbuilder.h b/icu4c/source/i18n/number_stringbuilder.h index cd8ce2f805e..b14ad9ede2f 100644 --- a/icu4c/source/i18n/number_stringbuilder.h +++ b/icu4c/source/i18n/number_stringbuilder.h @@ -106,6 +106,8 @@ class U_I18N_API NumberStringBuilder : public UMemory { void getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const; + bool containsField(Field field) const; + private: bool fUsingHeap = false; ValueOrHeapArray fChars; diff --git a/icu4c/source/i18n/number_types.h b/icu4c/source/i18n/number_types.h index 57da72f8aa0..8e39936e4e1 100644 --- a/icu4c/source/i18n/number_types.h +++ b/icu4c/source/i18n/number_types.h @@ -16,6 +16,7 @@ #include "uassert.h" #include "unicode/platform.h" #include "unicode/uniset.h" +#include "standardplural.h" U_NAMESPACE_BEGIN namespace number { namespace impl { @@ -45,6 +46,7 @@ class Modifier; class MutablePatternModifier; class DecimalQuantity; class NumberStringBuilder; +class ModifierStore; struct MicroProps; @@ -127,12 +129,13 @@ class U_I18N_API AffixPatternProvider { virtual bool hasBody() const = 0; }; + /** * A Modifier is an object that can be passed through the formatting pipeline until it is finally applied to the string * builder. A Modifier usually contains a prefix and a suffix that are applied, but it could contain something else, * like a {@link com.ibm.icu.text.SimpleFormatter} pattern. * - * A Modifier is usually immutable, except in cases such as {@link MurkyModifier}, which are mutable for performance + * A Modifier is usually immutable, except in cases such as {@link MutablePatternModifier}, which are mutable for performance * reasons. * * Exported as U_I18N_API because it is a base class for other exported types @@ -162,12 +165,12 @@ class U_I18N_API Modifier { * * @return The number of characters (UTF-16 code units) in the prefix. */ - virtual int32_t getPrefixLength(UErrorCode& status) const = 0; + virtual int32_t getPrefixLength() const = 0; /** * Returns the number of code points in the modifier, prefix plus suffix. */ - virtual int32_t getCodePointCount(UErrorCode& status) const = 0; + virtual int32_t getCodePointCount() const = 0; /** * Whether this modifier is strong. If a modifier is strong, it should always be applied immediately and not allowed @@ -177,8 +180,57 @@ class U_I18N_API Modifier { * @return Whether the modifier is strong. */ virtual bool isStrong() const = 0; + + /** + * Whether the modifier contains at least one occurrence of the given field. + */ + virtual bool containsField(UNumberFormatFields field) const = 0; + + /** + * A fill-in for getParameters(). obj will always be set; if non-null, the other + * two fields are also safe to read. + */ + struct Parameters { + const ModifierStore* obj = nullptr; + int8_t signum; + StandardPlural::Form plural; + + Parameters(); + Parameters(const ModifierStore* _obj, int8_t _signum, StandardPlural::Form _plural); + }; + + /** + * Gets a set of "parameters" for this Modifier. + * + * TODO: Make this return a `const Parameters*` more like Java? + */ + virtual void getParameters(Parameters& output) const = 0; + + /** + * Returns whether this Modifier is *semantically equivalent* to the other Modifier; + * in many cases, this is the same as equal, but parameters should be ignored. + */ + virtual bool semanticallyEquivalent(const Modifier& other) const = 0; }; + +/** + * This is *not* a modifier; rather, it is an object that can return modifiers + * based on given parameters. + * + * Exported as U_I18N_API because it is a base class for other exported types. + */ +class U_I18N_API ModifierStore { + public: + virtual ~ModifierStore(); + + /** + * Returns a Modifier with the given parameters (best-effort). + */ + virtual const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const = 0; +}; + + /** * This interface is used when all number formatting settings, including the locale, are known, except for the quantity * itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the diff --git a/icu4c/source/i18n/numrange_fluent.cpp b/icu4c/source/i18n/numrange_fluent.cpp new file mode 100644 index 00000000000..aee9518fe09 --- /dev/null +++ b/icu4c/source/i18n/numrange_fluent.cpp @@ -0,0 +1,430 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +// Allow implicit conversion from char16_t* to UnicodeString for this file: +// Helpful in toString methods and elsewhere. +#define UNISTR_FROM_STRING_EXPLICIT + +#include "numrange_impl.h" +#include "util.h" +#include "number_utypes.h" + +using namespace icu; +using namespace icu::number; +using namespace icu::number::impl; + + +// This function needs to be declared in this namespace so it can be friended. +// NOTE: In Java, this logic is handled in the resolve() function. +void icu::number::impl::touchRangeLocales(RangeMacroProps& macros) { + macros.formatter1.fMacros.locale = macros.locale; + macros.formatter2.fMacros.locale = macros.locale; +} + + +template +Derived NumberRangeFormatterSettings::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter1 = formatter; + copy.fMacros.singleFormatter = true; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter1 = formatter; + move.fMacros.singleFormatter = true; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterBoth(UnlocalizedNumberFormatter&& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter1 = std::move(formatter); + copy.fMacros.singleFormatter = true; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterBoth(UnlocalizedNumberFormatter&& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter1 = std::move(formatter); + move.fMacros.singleFormatter = true; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterFirst(const UnlocalizedNumberFormatter& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter1 = formatter; + copy.fMacros.singleFormatter = false; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterFirst(const UnlocalizedNumberFormatter& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter1 = formatter; + move.fMacros.singleFormatter = false; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterFirst(UnlocalizedNumberFormatter&& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter1 = std::move(formatter); + copy.fMacros.singleFormatter = false; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterFirst(UnlocalizedNumberFormatter&& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter1 = std::move(formatter); + move.fMacros.singleFormatter = false; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterSecond(const UnlocalizedNumberFormatter& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter2 = formatter; + copy.fMacros.singleFormatter = false; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterSecond(const UnlocalizedNumberFormatter& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter2 = formatter; + move.fMacros.singleFormatter = false; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterSecond(UnlocalizedNumberFormatter&& formatter) const& { + Derived copy(*this); + copy.fMacros.formatter2 = std::move(formatter); + copy.fMacros.singleFormatter = false; + touchRangeLocales(copy.fMacros); + return copy; +} + +template +Derived NumberRangeFormatterSettings::numberFormatterSecond(UnlocalizedNumberFormatter&& formatter) && { + Derived move(std::move(*this)); + move.fMacros.formatter2 = std::move(formatter); + move.fMacros.singleFormatter = false; + touchRangeLocales(move.fMacros); + return move; +} + +template +Derived NumberRangeFormatterSettings::collapse(UNumberRangeCollapse collapse) const& { + Derived copy(*this); + copy.fMacros.collapse = collapse; + return copy; +} + +template +Derived NumberRangeFormatterSettings::collapse(UNumberRangeCollapse collapse) && { + Derived move(std::move(*this)); + move.fMacros.collapse = collapse; + return move; +} + +template +Derived NumberRangeFormatterSettings::identityFallback(UNumberRangeIdentityFallback identityFallback) const& { + Derived copy(*this); + copy.fMacros.identityFallback = identityFallback; + return copy; +} + +template +Derived NumberRangeFormatterSettings::identityFallback(UNumberRangeIdentityFallback identityFallback) && { + Derived move(std::move(*this)); + move.fMacros.identityFallback = identityFallback; + return move; +} + +// Declare all classes that implement NumberRangeFormatterSettings +// See https://stackoverflow.com/a/495056/1407170 +template +class icu::number::NumberRangeFormatterSettings; +template +class icu::number::NumberRangeFormatterSettings; + + +UnlocalizedNumberRangeFormatter NumberRangeFormatter::with() { + UnlocalizedNumberRangeFormatter result; + return result; +} + +LocalizedNumberRangeFormatter NumberRangeFormatter::withLocale(const Locale& locale) { + return with().locale(locale); +} + + +template using NFS = NumberRangeFormatterSettings; +using LNF = LocalizedNumberRangeFormatter; +using UNF = UnlocalizedNumberRangeFormatter; + +UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(const UNF& other) + : UNF(static_cast&>(other)) {} + +UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(const NFS& other) + : NFS(other) { + // No additional fields to assign +} + +// Make default copy constructor call the NumberRangeFormatterSettings copy constructor. +UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(UNF&& src) U_NOEXCEPT + : UNF(static_cast&&>(src)) {} + +UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(NFS&& src) U_NOEXCEPT + : NFS(std::move(src)) { + // No additional fields to assign +} + +UnlocalizedNumberRangeFormatter& UnlocalizedNumberRangeFormatter::operator=(const UNF& other) { + NFS::operator=(static_cast&>(other)); + // No additional fields to assign + return *this; +} + +UnlocalizedNumberRangeFormatter& UnlocalizedNumberRangeFormatter::operator=(UNF&& src) U_NOEXCEPT { + NFS::operator=(static_cast&&>(src)); + // No additional fields to assign + return *this; +} + +// Make default copy constructor call the NumberRangeFormatterSettings copy constructor. +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const LNF& other) + : LNF(static_cast&>(other)) {} + +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const NFS& other) + : NFS(other) { + // No additional fields to assign +} + +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT + : LNF(static_cast&&>(src)) {} + +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(NFS&& src) U_NOEXCEPT + : NFS(std::move(src)) { + // No additional fields to assign +} + +LocalizedNumberRangeFormatter& LocalizedNumberRangeFormatter::operator=(const LNF& other) { + NFS::operator=(static_cast&>(other)); + // No additional fields to assign + return *this; +} + +LocalizedNumberRangeFormatter& LocalizedNumberRangeFormatter::operator=(LNF&& src) U_NOEXCEPT { + NFS::operator=(static_cast&&>(src)); + // No additional fields to assign + return *this; +} + + +LocalizedNumberRangeFormatter::~LocalizedNumberRangeFormatter() { + delete fImpl; +} + +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const RangeMacroProps& macros, const Locale& locale) { + fMacros = macros; + fMacros.locale = locale; + touchRangeLocales(fMacros); +} + +LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(RangeMacroProps&& macros, const Locale& locale) { + fMacros = std::move(macros); + fMacros.locale = locale; + touchRangeLocales(fMacros); +} + +LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale) const& { + return LocalizedNumberRangeFormatter(fMacros, locale); +} + +LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale)&& { + return LocalizedNumberRangeFormatter(std::move(fMacros), locale); +} + + +FormattedNumberRange LocalizedNumberRangeFormatter::formatFormattableRange( + const Formattable& first, const Formattable& second, UErrorCode& status) const { + if (U_FAILURE(status)) { + return FormattedNumberRange(U_ILLEGAL_ARGUMENT_ERROR); + } + + auto results = new UFormattedNumberRangeData(); + if (results == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return FormattedNumberRange(status); + } + + first.populateDecimalQuantity(results->quantity1, status); + if (U_FAILURE(status)) { + return FormattedNumberRange(status); + } + + second.populateDecimalQuantity(results->quantity2, status); + if (U_FAILURE(status)) { + return FormattedNumberRange(status); + } + + formatImpl(*results, first == second, status); + + // Do not save the results object if we encountered a failure. + if (U_SUCCESS(status)) { + return FormattedNumberRange(results); + } else { + delete results; + return FormattedNumberRange(status); + } +} + +void LocalizedNumberRangeFormatter::formatImpl( + UFormattedNumberRangeData& results, bool equalBeforeRounding, UErrorCode& status) const { + if (fImpl == nullptr) { + // TODO: Fix this once the atomic is ready! + auto* nonConstThis = const_cast(this); + nonConstThis->fImpl = new NumberRangeFormatterImpl(fMacros, status); + if (U_FAILURE(status)) { + return; + } + if (fImpl == nullptr) { + status = U_MEMORY_ALLOCATION_ERROR; + return; + } + } + fImpl->format(results, equalBeforeRounding, status); +} + + +FormattedNumberRange::FormattedNumberRange(FormattedNumberRange&& 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; +} + +FormattedNumberRange& FormattedNumberRange::operator=(FormattedNumberRange&& 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 FormattedNumberRange::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(); +} + +Appendable& FormattedNumberRange::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 FormattedNumberRange::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 FormattedNumberRange::getAllFieldPositions(FieldPositionIterator& iterator, UErrorCode& status) const { + FieldPositionIteratorHandler fpih(&iterator, status); + getAllFieldPositionsImpl(fpih, status); +} + +void FormattedNumberRange::getAllFieldPositionsImpl( + FieldPositionIteratorHandler& fpih, UErrorCode& status) const { + if (U_FAILURE(status)) { + return; + } + if (fResults == nullptr) { + status = fErrorCode; + return; + } + fResults->string.getAllFieldPositions(fpih, status); +} + +UnicodeString FormattedNumberRange::getFirstDecimal(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->quantity1.toScientificString(); +} + +UnicodeString FormattedNumberRange::getSecondDecimal(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->quantity2.toScientificString(); +} + +UNumberRangeIdentityResult FormattedNumberRange::getIdentityResult(UErrorCode& status) const { + if (U_FAILURE(status)) { + return UNUM_IDENTITY_RESULT_NOT_EQUAL; + } + if (fResults == nullptr) { + status = fErrorCode; + return UNUM_IDENTITY_RESULT_NOT_EQUAL; + } + return fResults->identityResult; +} + +FormattedNumberRange::~FormattedNumberRange() { + delete fResults; +} + + + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/numrange_impl.cpp b/icu4c/source/i18n/numrange_impl.cpp new file mode 100644 index 00000000000..19b8b10321f --- /dev/null +++ b/icu4c/source/i18n/numrange_impl.cpp @@ -0,0 +1,483 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +// Allow implicit conversion from char16_t* to UnicodeString for this file: +// Helpful in toString methods and elsewhere. +#define UNISTR_FROM_STRING_EXPLICIT + +#include "unicode/numberrangeformatter.h" +#include "numrange_impl.h" +#include "patternprops.h" +#include "uresimp.h" +#include "util.h" + +using namespace icu; +using namespace icu::number; +using namespace icu::number::impl; + +namespace { + +// Helper function for 2-dimensional switch statement +constexpr int8_t identity2d(UNumberRangeIdentityFallback a, UNumberRangeIdentityResult b) { + return static_cast(a) | (static_cast(b) << 4); +} + + +struct NumberRangeData { + SimpleFormatter rangePattern; + SimpleFormatter approximatelyPattern; +}; + +class NumberRangeDataSink : public ResourceSink { + public: + NumberRangeDataSink(NumberRangeData& data) : fData(data) {} + + void put(const char* key, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE { + ResourceTable miscTable = value.getTable(status); + if (U_FAILURE(status)) { return; } + for (int i = 0; miscTable.getKeyAndValue(i, key, value); i++) { + if (uprv_strcmp(key, "range") == 0) { + if (fData.rangePattern.getArgumentLimit() != 0) { + continue; // have already seen this pattern + } + fData.rangePattern = {value.getUnicodeString(status), status}; + } else if (uprv_strcmp(key, "approximately") == 0) { + if (fData.approximatelyPattern.getArgumentLimit() != 0) { + continue; // have already seen this pattern + } + fData.approximatelyPattern = {value.getUnicodeString(status), status}; + } + } + } + + private: + NumberRangeData& fData; +}; + +void getNumberRangeData(const char* localeName, const char* nsName, NumberRangeData& data, UErrorCode& status) { + if (U_FAILURE(status)) { return; } + LocalUResourceBundlePointer rb(ures_open(NULL, localeName, &status)); + if (U_FAILURE(status)) { return; } + NumberRangeDataSink sink(data); + + CharString dataPath; + dataPath.append("NumberElements/", -1, status); + dataPath.append(nsName, -1, status); + dataPath.append("/miscPatterns", -1, status); + ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status); + if (U_FAILURE(status)) { return; } + + // TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that? + + if (data.rangePattern.getArgumentLimit() == 0) { + // No data! + data.rangePattern = {u"{0}–{1}", status}; + } + if (data.approximatelyPattern.getArgumentLimit() == 0) { + // No data! + data.approximatelyPattern = {u"~{0}", status}; + } +} + +class PluralRangesDataSink : public ResourceSink { + public: + PluralRangesDataSink(StandardPluralRanges& output) : fOutput(output) {} + + void put(const char* /*key*/, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE { + ResourceArray entriesArray = value.getArray(status); + if (U_FAILURE(status)) { return; } + fOutput.setCapacity(entriesArray.getSize()); + for (int i = 0; entriesArray.getValue(i, value); i++) { + ResourceArray pluralFormsArray = value.getArray(status); + if (U_FAILURE(status)) { return; } + pluralFormsArray.getValue(0, value); + StandardPlural::Form first = StandardPlural::fromString(value.getUnicodeString(status), status); + if (U_FAILURE(status)) { return; } + pluralFormsArray.getValue(1, value); + StandardPlural::Form second = StandardPlural::fromString(value.getUnicodeString(status), status); + if (U_FAILURE(status)) { return; } + pluralFormsArray.getValue(2, value); + StandardPlural::Form result = StandardPlural::fromString(value.getUnicodeString(status), status); + if (U_FAILURE(status)) { return; } + fOutput.addPluralRange(first, second, result); + } + } + + private: + StandardPluralRanges& fOutput; +}; + +void getPluralRangesData(const Locale& locale, StandardPluralRanges& output, UErrorCode& status) { + if (U_FAILURE(status)) { return; } + LocalUResourceBundlePointer rb(ures_openDirect(nullptr, "pluralRanges", &status)); + if (U_FAILURE(status)) { return; } + + CharString dataPath; + dataPath.append("locales/", -1, status); + dataPath.append(locale.getLanguage(), -1, status); + if (U_FAILURE(status)) { return; } + int32_t setLen; + // Not all languages are covered: fail gracefully + UErrorCode internalStatus = U_ZERO_ERROR; + const UChar* set = ures_getStringByKeyWithFallback(rb.getAlias(), dataPath.data(), &setLen, &internalStatus); + if (U_FAILURE(internalStatus)) { return; } + + dataPath.clear(); + dataPath.append("rules/", -1, status); + dataPath.appendInvariantChars(set, setLen, status); + if (U_FAILURE(status)) { return; } + PluralRangesDataSink sink(output); + ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status); + if (U_FAILURE(status)) { return; } +} + +} // namespace + + +void StandardPluralRanges::initialize(const Locale& locale, UErrorCode& status) { + getPluralRangesData(locale, *this, status); +} + +void StandardPluralRanges::addPluralRange( + StandardPlural::Form first, + StandardPlural::Form second, + StandardPlural::Form result) { + U_ASSERT(fTriplesLen < fTriples.getCapacity()); + fTriples[fTriplesLen] = {first, second, result}; + fTriplesLen++; +} + +void StandardPluralRanges::setCapacity(int32_t length) { + if (length > fTriples.getCapacity()) { + fTriples.resize(length, 0); + } +} + +StandardPlural::Form +StandardPluralRanges::resolve(StandardPlural::Form first, StandardPlural::Form second) const { + for (int32_t i=0; isemanticallyEquivalent(*micros2.modInner) + || !micros1.modMiddle->semanticallyEquivalent(*micros2.modMiddle) + || !micros1.modOuter->semanticallyEquivalent(*micros2.modOuter)) { + formatRange(data, micros1, micros2, status); + data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL; + return; + } + + // Check for identity + if (equalBeforeRounding) { + data.identityResult = UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING; + } else if (data.quantity1 == data.quantity2) { + data.identityResult = UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING; + } else { + data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL; + } + + switch (identity2d(fIdentityFallback, data.identityResult)) { + case identity2d(UNUM_IDENTITY_FALLBACK_RANGE, + UNUM_IDENTITY_RESULT_NOT_EQUAL): + case identity2d(UNUM_IDENTITY_FALLBACK_RANGE, + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_RANGE, + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY, + UNUM_IDENTITY_RESULT_NOT_EQUAL): + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_NOT_EQUAL): + case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_NOT_EQUAL): + formatRange(data, micros1, micros2, status); + break; + + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY, + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY, + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING): + formatApproximately(data, micros1, micros2, status); + break; + + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING): + formatSingleValue(data, micros1, micros2, status); + break; + + default: + U_ASSERT(false); + break; + } +} + + +void NumberRangeFormatterImpl::formatSingleValue(UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const { + if (U_FAILURE(status)) { return; } + if (fSameFormatters) { + int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status); + NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status); + } else { + formatRange(data, micros1, micros2, status); + } +} + + +void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const { + if (U_FAILURE(status)) { return; } + if (fSameFormatters) { + int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status); + length += NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status); + fApproximatelyModifier.apply(data.string, 0, length, status); + } else { + formatRange(data, micros1, micros2, status); + } +} + + +void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const { + if (U_FAILURE(status)) { return; } + + // modInner is always notation (scientific); collapsable in ALL. + // modOuter is always units; collapsable in ALL, AUTO, and UNIT. + // modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT. + // Never collapse an outer mod but not an inner mod. + bool collapseOuter, collapseMiddle, collapseInner; + switch (fCollapse) { + case UNUM_RANGE_COLLAPSE_ALL: + case UNUM_RANGE_COLLAPSE_AUTO: + case UNUM_RANGE_COLLAPSE_UNIT: + { + // OUTER MODIFIER + collapseOuter = micros1.modOuter->semanticallyEquivalent(*micros2.modOuter); + + if (!collapseOuter) { + // Never collapse inner mods if outer mods are not collapsable + collapseMiddle = false; + collapseInner = false; + break; + } + + // MIDDLE MODIFIER + collapseMiddle = micros1.modMiddle->semanticallyEquivalent(*micros2.modMiddle); + + if (!collapseMiddle) { + // Never collapse inner mods if outer mods are not collapsable + collapseInner = false; + break; + } + + // MIDDLE MODIFIER HEURISTICS + // (could disable collapsing of the middle modifier) + // The modifiers are equal by this point, so we can look at just one of them. + const Modifier* mm = micros1.modMiddle; + if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) { + // Only collapse if the modifier is a unit. + // TODO: Make a better way to check for a unit? + // TODO: Handle case where the modifier has both notation and unit (compact currency)? + if (!mm->containsField(UNUM_CURRENCY_FIELD) && !mm->containsField(UNUM_PERCENT_FIELD)) { + collapseMiddle = false; + } + } else if (fCollapse == UNUM_RANGE_COLLAPSE_AUTO) { + // Heuristic as of ICU 63: collapse only if the modifier is more than one code point. + if (mm->getCodePointCount() <= 1) { + collapseMiddle = false; + } + } + + if (!collapseMiddle || fCollapse != UNUM_RANGE_COLLAPSE_ALL) { + collapseInner = false; + break; + } + + // INNER MODIFIER + collapseInner = micros1.modInner->semanticallyEquivalent(*micros2.modInner); + + // All done checking for collapsability. + break; + } + + default: + collapseOuter = false; + collapseMiddle = false; + collapseInner = false; + break; + } + + NumberStringBuilder& string = data.string; + int32_t lengthPrefix = 0; + int32_t length1 = 0; + int32_t lengthInfix = 0; + int32_t length2 = 0; + int32_t lengthSuffix = 0; + + // Use #define so that these are evaluated at the call site. + #define UPRV_INDEX_0 (lengthPrefix) + #define UPRV_INDEX_1 (lengthPrefix + length1) + #define UPRV_INDEX_2 (lengthPrefix + length1 + lengthInfix) + #define UPRV_INDEX_3 (lengthPrefix + length1 + lengthInfix + length2) + + int32_t lengthRange = SimpleModifier::formatTwoArgPattern( + fRangeFormatter, + string, + 0, + &lengthPrefix, + &lengthSuffix, + UNUM_FIELD_COUNT, + status); + if (U_FAILURE(status)) { return; } + lengthInfix = lengthRange - lengthPrefix - lengthSuffix; + U_ASSERT(lengthInfix > 0); + + // SPACING HEURISTIC + // Add spacing unless all modifiers are collapsed. + // TODO: add API to control this? + // TODO: Use a data-driven heuristic like currency spacing? + // TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications) + { + bool repeatInner = !collapseInner && micros1.modInner->getCodePointCount() > 0; + bool repeatMiddle = !collapseMiddle && micros1.modMiddle->getCodePointCount() > 0; + bool repeatOuter = !collapseOuter && micros1.modOuter->getCodePointCount() > 0; + if (repeatInner || repeatMiddle || repeatOuter) { + // Add spacing if there is not already spacing + if (!PatternProps::isWhiteSpace(string.charAt(UPRV_INDEX_1))) { + lengthInfix += string.insertCodePoint(UPRV_INDEX_1, u'\u0020', UNUM_FIELD_COUNT, status); + } + if (!PatternProps::isWhiteSpace(string.charAt(UPRV_INDEX_2 - 1))) { + lengthInfix += string.insertCodePoint(UPRV_INDEX_2, u'\u0020', UNUM_FIELD_COUNT, status); + } + } + } + + length1 += NumberFormatterImpl::writeNumber(micros1, data.quantity1, string, UPRV_INDEX_0, status); + length2 += NumberFormatterImpl::writeNumber(micros2, data.quantity2, string, UPRV_INDEX_2, status); + + // TODO: Support padding? + + if (collapseInner) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + const Modifier& mod = resolveModifierPlurals(*micros1.modInner, *micros2.modInner); + lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status); + } else { + length1 += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status); + length2 += micros2.modInner->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status); + } + + if (collapseMiddle) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + const Modifier& mod = resolveModifierPlurals(*micros1.modMiddle, *micros2.modMiddle); + lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status); + } else { + length1 += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status); + length2 += micros2.modMiddle->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status); + } + + if (collapseOuter) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + const Modifier& mod = resolveModifierPlurals(*micros1.modOuter, *micros2.modOuter); + lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status); + } else { + length1 += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status); + length2 += micros2.modOuter->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status); + } +} + + +const Modifier& +NumberRangeFormatterImpl::resolveModifierPlurals(const Modifier& first, const Modifier& second) const { + Modifier::Parameters parameters; + first.getParameters(parameters); + if (parameters.obj == nullptr) { + // No plural form; return a fallback (e.g., the first) + return first; + } + StandardPlural::Form firstPlural = parameters.plural; + + second.getParameters(parameters); + if (parameters.obj == nullptr) { + // No plural form; return a fallback (e.g., the first) + return first; + } + StandardPlural::Form secondPlural = parameters.plural; + + // Get the required plural form from data + StandardPlural::Form resultPlural = fPluralRanges.resolve(firstPlural, secondPlural); + + // Get and return the new Modifier + const Modifier* mod = parameters.obj->getModifier(parameters.signum, resultPlural); + U_ASSERT(mod != nullptr); + return *mod; +} + + + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/numrange_impl.h b/icu4c/source/i18n/numrange_impl.h new file mode 100644 index 00000000000..787fc656860 --- /dev/null +++ b/icu4c/source/i18n/numrange_impl.h @@ -0,0 +1,114 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING +#ifndef __SOURCE_NUMRANGE_TYPES_H__ +#define __SOURCE_NUMRANGE_TYPES_H__ + +#include "unicode/numberformatter.h" +#include "unicode/numberrangeformatter.h" +#include "unicode/simpleformatter.h" +#include "number_types.h" +#include "number_decimalquantity.h" +#include "number_formatimpl.h" +#include "number_stringbuilder.h" + +U_NAMESPACE_BEGIN namespace number { +namespace impl { + + +/** + * Class similar to UFormattedNumberData. + * + * Has incomplete magic number logic that will need to be finished + * if this is to be exposed as C API in the future. + */ +struct UFormattedNumberRangeData : public UMemory { + // The magic number to identify incoming objects. + // Reads in ASCII as "FDR" (FormatteDnumberRange with room at the end) + static constexpr int32_t kMagic = 0x46445200; + + // Data members: + int32_t fMagic = kMagic; + DecimalQuantity quantity1; + DecimalQuantity quantity2; + NumberStringBuilder string; + UNumberRangeIdentityResult identityResult = UNUM_IDENTITY_RESULT_COUNT; + + // No C conversion methods (no C API yet) +}; + + +class StandardPluralRanges : public UMemory { + public: + void initialize(const Locale& locale, UErrorCode& status); + StandardPlural::Form resolve(StandardPlural::Form first, StandardPlural::Form second) const; + + /** Used for data loading. */ + void addPluralRange( + StandardPlural::Form first, + StandardPlural::Form second, + StandardPlural::Form result); + + /** Used for data loading. */ + void setCapacity(int32_t length); + + private: + struct StandardPluralRangeTriple { + StandardPlural::Form first; + StandardPlural::Form second; + StandardPlural::Form result; + }; + + // TODO: An array is simple here, but it results in linear lookup time. + // Certain locales have 20-30 entries in this list. + // Consider changing to a smarter data structure. + typedef MaybeStackArray PluralRangeTriples; + PluralRangeTriples fTriples; + int32_t fTriplesLen = 0; +}; + + +class NumberRangeFormatterImpl : public UMemory { + public: + NumberRangeFormatterImpl(const RangeMacroProps& macros, UErrorCode& status); + + void format(UFormattedNumberRangeData& data, bool equalBeforeRounding, UErrorCode& status) const; + + private: + NumberFormatterImpl formatterImpl1; + NumberFormatterImpl formatterImpl2; + bool fSameFormatters; + + UNumberRangeCollapse fCollapse; + UNumberRangeIdentityFallback fIdentityFallback; + + SimpleFormatter fRangeFormatter; + SimpleModifier fApproximatelyModifier; + + StandardPluralRanges fPluralRanges; + + void formatSingleValue(UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const; + + void formatApproximately(UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const; + + void formatRange(UFormattedNumberRangeData& data, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const; + + const Modifier& resolveModifierPlurals(const Modifier& first, const Modifier& second) const; +}; + + +} // namespace impl +} // namespace number +U_NAMESPACE_END + +#endif //__SOURCE_NUMRANGE_TYPES_H__ +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 4dd03ce8472..b75d8e6432f 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -144,6 +144,9 @@ class MultiplierFormatHandler; class CurrencySymbols; class GeneratorHelpers; class DecNum; +class NumberRangeFormatterImpl; +struct RangeMacroProps; +void touchRangeLocales(impl::RangeMacroProps& macros); } // namespace impl @@ -1423,7 +1426,8 @@ struct U_I18N_API MacroProps : public UMemory { /** * An abstract base class for specifying settings related to number formatting. This class is implemented by - * {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}. + * {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}. This class is not intended for + * public subclassing. */ template class U_I18N_API NumberFormatterSettings { @@ -2108,6 +2112,10 @@ class U_I18N_API NumberFormatterSettings { friend class LocalizedNumberFormatter; friend class UnlocalizedNumberFormatter; + + // Give NumberRangeFormatter access to the MacroProps + friend void impl::touchRangeLocales(impl::RangeMacroProps& macros); + friend class impl::NumberRangeFormatterImpl; }; /** @@ -2124,13 +2132,6 @@ class U_I18N_API UnlocalizedNumberFormatter * Associate the given locale with the number formatter. The locale is used for picking the appropriate symbols, * formats, and other data for number display. * - *

- * To use the Java default locale, call Locale::getDefault(): - * - *

-     * NumberFormatter::with(). ... .locale(Locale::getDefault())
-     * 
- * * @param locale * The locale to use when loading data for number formatting. * @return The fluent chain. @@ -2156,7 +2157,6 @@ class U_I18N_API UnlocalizedNumberFormatter */ UnlocalizedNumberFormatter() = default; - // Make default copy constructor call the NumberFormatterSettings copy constructor. /** * Returns a copy of this UnlocalizedNumberFormatter. * @draft ICU 60 @@ -2295,7 +2295,6 @@ class U_I18N_API LocalizedNumberFormatter */ LocalizedNumberFormatter() = default; - // Make default copy constructor call the NumberFormatterSettings copy constructor. /** * Returns a copy of this LocalizedNumberFormatter. * @draft ICU 60 @@ -2457,9 +2456,9 @@ class U_I18N_API FormattedNumber : public UMemory { #endif /* U_HIDE_DEPRECATED_API */ /** - * Determines the start and end indices of the next occurrence of the given field in the - * output string. This allows you to determine the locations of, for example, the integer part, - * fraction part, or symbols. + * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given + * field in the output string. This allows you to determine the locations of, for example, + * the integer part, fraction part, or symbols. * * If a field occurs just once, calling this method will find that occurrence and return it. If a * field occurs multiple times, this method may be called repeatedly with the following pattern: @@ -2478,7 +2477,7 @@ class U_I18N_API FormattedNumber : public UMemory { * Input+output variable. On input, the "field" property determines which field to look * up, and the "beginIndex" and "endIndex" properties determine where to begin the search. * On output, the "beginIndex" is set to the beginning of the first occurrence of the - * field with either begin or end indices after the input indices, "endIndex" is set to + * field with either begin or end indices after the input indices; "endIndex" is set to * the end of that occurrence of the field (exclusive index). If a field position is not * found, the method returns FALSE and the FieldPosition may or may not be changed. * @param status diff --git a/icu4c/source/i18n/unicode/numberrangeformatter.h b/icu4c/source/i18n/unicode/numberrangeformatter.h new file mode 100644 index 00000000000..5b9f264f9bf --- /dev/null +++ b/icu4c/source/i18n/unicode/numberrangeformatter.h @@ -0,0 +1,849 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#if !UCONFIG_NO_FORMATTING +#ifndef __NUMBERRANGEFORMATTER_H__ +#define __NUMBERRANGEFORMATTER_H__ + +#include "unicode/appendable.h" +#include "unicode/fieldpos.h" +#include "unicode/fpositer.h" +#include "unicode/numberformatter.h" + +#ifndef U_HIDE_DRAFT_API + +/** + * \file + * \brief C++ API: Library for localized formatting of number, currency, and unit ranges. + * + * The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement. + *

+ * Usage example: + *

+ *

+ * NumberRangeFormatter::with()
+ *     .identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE)
+ *     .numberFormatterFirst(NumberFormatter::with().adoptUnit(MeasureUnit::createMeter()))
+ *     .numberFormatterSecond(NumberFormatter::with().adoptUnit(MeasureUnit::createKilometer()))
+ *     .locale("en-GB")
+ *     .formatRange(750, 1.2, status)
+ *     .toString(status);
+ * // => "750 m - 1.2 km"
+ * 
+ *

+ * Like NumberFormatter, NumberRangeFormatter instances are immutable and thread-safe. This API is based on the + * fluent design pattern popularized by libraries such as Google's Guava. + * + * @author Shane Carr + */ + + +/** + * Defines how to merge fields that are identical across the range sign. + * + * @draft ICU 63 + */ +typedef enum UNumberRangeCollapse { + /** + * Use locale data and heuristics to determine how much of the string to collapse. Could end up collapsing none, + * some, or all repeated pieces in a locale-sensitive way. + * + * The heuristics used for this option are subject to change over time. + * + * @draft ICU 63 + */ + UNUM_RANGE_COLLAPSE_AUTO, + + /** + * Do not collapse any part of the number. Example: "3.2 thousand kilograms – 5.3 thousand kilograms" + * + * @draft ICU 63 + */ + UNUM_RANGE_COLLAPSE_NONE, + + /** + * Collapse the unit part of the number, but not the notation, if present. Example: "3.2 thousand – 5.3 thousand + * kilograms" + * + * @draft ICU 63 + */ + UNUM_RANGE_COLLAPSE_UNIT, + + /** + * Collapse any field that is equal across the range sign. May introduce ambiguity on the magnitude of the + * number. Example: "3.2 – 5.3 thousand kilograms" + * + * @draft ICU 63 + */ + UNUM_RANGE_COLLAPSE_ALL +} UNumberRangeCollapse; + +/** + * Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect + * when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber. + * + * @draft ICU 63 + * @see NumberRangeFormatter + */ +typedef enum UNumberRangeIdentityFallback { + /** + * Show the number as a single value rather than a range. Example: "$5" + * + * @draft ICU 63 + */ + UNUM_IDENTITY_FALLBACK_SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding, + * show the single value. Example: "~$5" or "$5" + * + * @draft ICU 63 + */ + UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the + * inputs are the same. Example: "~$5" + * + * @draft ICU 63 + */ + UNUM_IDENTITY_FALLBACK_APPROXIMATELY, + + /** + * Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the + * same. Example (with RangeCollapse.NONE): "$5 – $5" + * + * @draft ICU 63 + */ + UNUM_IDENTITY_FALLBACK_RANGE +} UNumberRangeIdentityFallback; + +/** + * Used in the result class FormattedNumberRange to indicate to the user whether the numbers formatted in the range + * were equal or not, and whether or not the identity fallback was applied. + * + * @draft ICU 63 + * @see NumberRangeFormatter + */ +typedef enum UNumberRangeIdentityResult { + /** + * Used to indicate that the two numbers in the range were equal, even before any rounding rules were applied. + * + * @draft ICU 63 + * @see NumberRangeFormatter + */ + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING, + + /** + * Used to indicate that the two numbers in the range were equal, but only after rounding rules were applied. + * + * @draft ICU 63 + * @see NumberRangeFormatter + */ + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING, + + /** + * Used to indicate that the two numbers in the range were not equal, even after rounding rules were applied. + * + * @draft ICU 63 + * @see NumberRangeFormatter + */ + UNUM_IDENTITY_RESULT_NOT_EQUAL, + +#ifndef U_HIDE_INTERNAL_API + /** + * The number of entries in this enum. + * @internal + */ + UNUM_IDENTITY_RESULT_COUNT +#endif + +} UNumberRangeIdentityResult; + +U_NAMESPACE_BEGIN + +namespace number { // icu::number + +// Forward declarations: +class UnlocalizedNumberRangeFormatter; +class LocalizedNumberRangeFormatter; +class FormattedNumberRange; + +namespace impl { + +// Forward declarations: +struct RangeMacroProps; +class DecimalQuantity; +struct UFormattedNumberRangeData; +class NumberRangeFormatterImpl; + +} // namespace impl + +// Other helper classes would go here, but there are none. + +namespace impl { // icu::number::impl + +// Do not enclose entire MacroProps with #ifndef U_HIDE_INTERNAL_API, needed for a protected field +/** @internal */ +struct U_I18N_API RangeMacroProps : public UMemory { + /** @internal */ + UnlocalizedNumberFormatter formatter1; // = NumberFormatter::with(); + + /** @internal */ + UnlocalizedNumberFormatter formatter2; // = NumberFormatter::with(); + + /** @internal */ + bool singleFormatter = true; + + /** @internal */ + UNumberRangeCollapse collapse = UNUM_RANGE_COLLAPSE_AUTO; + + /** @internal */ + UNumberRangeIdentityFallback identityFallback = UNUM_IDENTITY_FALLBACK_APPROXIMATELY; + + /** @internal */ + Locale locale; + + // NOTE: Uses default copy and move constructors. + + /** + * Check all members for errors. + * @internal + */ + bool copyErrorTo(UErrorCode &status) const { + return formatter1.copyErrorTo(status) || formatter2.copyErrorTo(status); + } +}; + +} // namespace impl + +/** + * An abstract base class for specifying settings related to number formatting. This class is implemented by + * {@link UnlocalizedNumberRangeFormatter} and {@link LocalizedNumberRangeFormatter}. This class is not intended for + * public subclassing. + */ +template +class U_I18N_API NumberRangeFormatterSettings { + public: + /** + * Sets the NumberFormatter instance to use for the numbers in the range. The same formatter is applied to both + * sides of the range. + *

+ * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @draft ICU 63 + */ + Derived numberFormatterBoth(const UnlocalizedNumberFormatter &formatter) const &; + + /** + * Overload of numberFormatterBoth() for use on an rvalue reference. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @see #numberFormatterBoth + * @draft ICU 63 + */ + Derived numberFormatterBoth(const UnlocalizedNumberFormatter &formatter) &&; + + /** + * Overload of numberFormatterBoth() for use on an rvalue reference. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @see #numberFormatterBoth + * @draft ICU 63 + */ + Derived numberFormatterBoth(UnlocalizedNumberFormatter &&formatter) const &; + + /** + * Overload of numberFormatterBoth() for use on an rvalue reference. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @see #numberFormatterBoth + * @draft ICU 63 + */ + Derived numberFormatterBoth(UnlocalizedNumberFormatter &&formatter) &&; + + /** + * Sets the NumberFormatter instance to use for the first number in the range. + *

+ * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @return The fluent chain. + * @draft ICU 63 + */ + Derived numberFormatterFirst(const UnlocalizedNumberFormatter &formatterFirst) const &; + + /** + * Overload of numberFormatterFirst() for use on an rvalue reference. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @return The fluent chain. + * @see #numberFormatterFirst + * @draft ICU 63 + */ + Derived numberFormatterFirst(const UnlocalizedNumberFormatter &formatterFirst) &&; + + /** + * Overload of numberFormatterFirst() for use on an rvalue reference. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @return The fluent chain. + * @see #numberFormatterFirst + * @draft ICU 63 + */ + Derived numberFormatterFirst(UnlocalizedNumberFormatter &&formatterFirst) const &; + + /** + * Overload of numberFormatterFirst() for use on an rvalue reference. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @return The fluent chain. + * @see #numberFormatterFirst + * @draft ICU 63 + */ + Derived numberFormatterFirst(UnlocalizedNumberFormatter &&formatterFirst) &&; + + /** + * Sets the NumberFormatter instance to use for the second number in the range. + *

+ * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @draft ICU 63 + */ + Derived numberFormatterSecond(const UnlocalizedNumberFormatter &formatterSecond) const &; + + /** + * Overload of numberFormatterSecond() for use on an rvalue reference. + * + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @see #numberFormatterSecond + * @draft ICU 63 + */ + Derived numberFormatterSecond(const UnlocalizedNumberFormatter &formatterSecond) &&; + + /** + * Overload of numberFormatterSecond() for use on an rvalue reference. + * + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @see #numberFormatterSecond + * @draft ICU 63 + */ + Derived numberFormatterSecond(UnlocalizedNumberFormatter &&formatterSecond) const &; + + /** + * Overload of numberFormatterSecond() for use on an rvalue reference. + * + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @see #numberFormatterSecond + * @draft ICU 63 + */ + Derived numberFormatterSecond(UnlocalizedNumberFormatter &&formatterSecond) &&; + + /** + * Sets the aggressiveness of "collapsing" fields across the range separator. Possible values: + *

+ *

    + *
  • ALL: "3-5K miles"
  • + *
  • UNIT: "3K - 5K miles"
  • + *
  • NONE: "3K miles - 5K miles"
  • + *
  • AUTO: usually UNIT or NONE, depending on the locale and formatter settings
  • + *
+ *

+ * The default value is AUTO. + * + * @param collapse + * The collapsing strategy to use for this range. + * @return The fluent chain. + * @draft ICU 63 + */ + Derived collapse(UNumberRangeCollapse collapse) const &; + + /** + * Overload of collapse() for use on an rvalue reference. + * + * @param collapse + * The collapsing strategy to use for this range. + * @return The fluent chain. + * @see #collapse + * @draft ICU 63 + */ + Derived collapse(UNumberRangeCollapse collapse) &&; + + /** + * Sets the behavior when the two sides of the range are the same. This could happen if the same two numbers are + * passed to the formatRange function, or if different numbers are passed to the function but they become the same + * after rounding rules are applied. Possible values: + *

+ *

    + *
  • SINGLE_VALUE: "5 miles"
  • + *
  • APPROXIMATELY_OR_SINGLE_VALUE: "~5 miles" or "5 miles", depending on whether the number was the same before + * rounding was applied
  • + *
  • APPROXIMATELY: "~5 miles"
  • + *
  • RANGE: "5-5 miles" (with collapse=UNIT)
  • + *
+ *

+ * The default value is APPROXIMATELY. + * + * @param identityFallback + * The strategy to use when formatting two numbers that end up being the same. + * @return The fluent chain. + * @draft ICU 63 + */ + Derived identityFallback(UNumberRangeIdentityFallback identityFallback) const &; + + /** + * Overload of identityFallback() for use on an rvalue reference. + * + * @param identityFallback + * The strategy to use when formatting two numbers that end up being the same. + * @return The fluent chain. + * @see #identityFallback + * @draft ICU 63 + */ + Derived identityFallback(UNumberRangeIdentityFallback identityFallback) &&; + + /** + * Sets the UErrorCode if an error occurred in the fluent chain. + * Preserves older error codes in the outErrorCode. + * @return TRUE if U_FAILURE(outErrorCode) + * @draft ICU 63 + */ + UBool copyErrorTo(UErrorCode &outErrorCode) const { + if (U_FAILURE(outErrorCode)) { + // Do not overwrite the older error code + return TRUE; + } + fMacros.copyErrorTo(outErrorCode); + return U_FAILURE(outErrorCode); + }; + + // NOTE: Uses default copy and move constructors. + + private: + impl::RangeMacroProps fMacros; + + // Don't construct me directly! Use (Un)LocalizedNumberFormatter. + NumberRangeFormatterSettings() = default; + + friend class LocalizedNumberRangeFormatter; + friend class UnlocalizedNumberRangeFormatter; +}; + +/** + * A NumberRangeFormatter that does not yet have a locale. In order to format, a locale must be specified. + * + * @see NumberRangeFormatter + * @draft ICU 63 + */ +class U_I18N_API UnlocalizedNumberRangeFormatter + : public NumberRangeFormatterSettings, public UMemory { + + public: + /** + * Associate the given locale with the number range formatter. The locale is used for picking the + * appropriate symbols, formats, and other data for number display. + * + * @param locale + * The locale to use when loading data for number formatting. + * @return The fluent chain. + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter locale(const icu::Locale &locale) const &; + + /** + * Overload of locale() for use on an rvalue reference. + * + * @param locale + * The locale to use when loading data for number formatting. + * @return The fluent chain. + * @see #locale + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter locale(const icu::Locale &locale) &&; + + /** + * Default constructor: puts the formatter into a valid but undefined state. + * + * @draft ICU 63 + */ + UnlocalizedNumberRangeFormatter() = default; + + /** + * Returns a copy of this UnlocalizedNumberRangeFormatter. + * @draft ICU 63 + */ + UnlocalizedNumberRangeFormatter(const UnlocalizedNumberRangeFormatter &other); + + /** + * Move constructor: + * The source UnlocalizedNumberRangeFormatter will be left in a valid but undefined state. + * @draft ICU 63 + */ + UnlocalizedNumberRangeFormatter(UnlocalizedNumberRangeFormatter&& src) U_NOEXCEPT; + + /** + * Copy assignment operator. + * @draft ICU 63 + */ + UnlocalizedNumberRangeFormatter& operator=(const UnlocalizedNumberRangeFormatter& other); + + /** + * Move assignment operator: + * The source UnlocalizedNumberRangeFormatter will be left in a valid but undefined state. + * @draft ICU 63 + */ + UnlocalizedNumberRangeFormatter& operator=(UnlocalizedNumberRangeFormatter&& src) U_NOEXCEPT; + + private: + explicit UnlocalizedNumberRangeFormatter( + const NumberRangeFormatterSettings& other); + + explicit UnlocalizedNumberRangeFormatter( + NumberRangeFormatterSettings&& src) U_NOEXCEPT; + + // To give the fluent setters access to this class's constructor: + friend class NumberRangeFormatterSettings; + + // To give NumberRangeFormatter::with() access to this class's constructor: + friend class NumberRangeFormatter; +}; + +/** + * A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available. + * + * @see NumberFormatter + * @draft ICU 63 + */ +class U_I18N_API LocalizedNumberRangeFormatter + : public NumberRangeFormatterSettings, public UMemory { + public: + /** + * Format the given Formattables to a string using the settings specified in the NumberRangeFormatter fluent setting + * chain. + * + * @param first + * The first number in the range, usually to the left in LTR locales. + * @param second + * The second number in the range, usually to the right in LTR locales. + * @param status + * Set if an error occurs while formatting. + * @return A FormattedNumberRange object; call .toString() to get the string. + * @draft ICU 63 + */ + FormattedNumberRange formatFormattableRange( + const Formattable& first, const Formattable& second, UErrorCode& status) const; + + /** + * Default constructor: puts the formatter into a valid but undefined state. + * + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter() = default; + + /** + * Returns a copy of this LocalizedNumberRangeFormatter. + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter(const LocalizedNumberRangeFormatter &other); + + /** + * Move constructor: + * The source LocalizedNumberRangeFormatter will be left in a valid but undefined state. + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT; + + /** + * Copy assignment operator. + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter& operator=(const LocalizedNumberRangeFormatter& other); + + /** + * Move assignment operator: + * The source LocalizedNumberRangeFormatter will be left in a valid but undefined state. + * @draft ICU 63 + */ + LocalizedNumberRangeFormatter& operator=(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT; + +#ifndef U_HIDE_INTERNAL_API + + /** + * @param results + * The results object. This method will mutate it to save the results. + * @param status + * Set if an error occurs while formatting. + * @internal + */ + void formatImpl(impl::UFormattedNumberRangeData& results, bool equalBeforeRounding, + UErrorCode& status) const; + +#endif + + /** + * Destruct this LocalizedNumberRangeFormatter, cleaning up any memory it might own. + * @draft ICU 63 + */ + ~LocalizedNumberRangeFormatter(); + + private: + // TODO: This is not thread-safe! Do NOT check this in without an atomic here. + impl::NumberRangeFormatterImpl* fImpl = nullptr; + + explicit LocalizedNumberRangeFormatter( + const NumberRangeFormatterSettings& other); + + explicit LocalizedNumberRangeFormatter( + NumberRangeFormatterSettings&& src) U_NOEXCEPT; + + LocalizedNumberRangeFormatter(const impl::RangeMacroProps ¯os, const Locale &locale); + + LocalizedNumberRangeFormatter(impl::RangeMacroProps &¯os, const Locale &locale); + + // To give the fluent setters access to this class's constructor: + friend class NumberRangeFormatterSettings; + friend class NumberRangeFormatterSettings; + + // To give UnlocalizedNumberRangeFormatter::locale() access to this class's constructor: + friend class UnlocalizedNumberRangeFormatter; +}; + +/** + * The result of a number range formatting operation. This class allows the result to be exported in several data types, + * including a UnicodeString and a FieldPositionIterator. + * + * @draft ICU 63 + */ +class U_I18N_API FormattedNumberRange : public UMemory { + public: + /** + * Returns a UnicodeString representation of the formatted number range. + * + * @param status + * Set if an error occurs while formatting the number to the UnicodeString. + * @return a UnicodeString containing the localized number range. + * @draft ICU 63 + */ + UnicodeString toString(UErrorCode& status) const; + + /** + * Appends the formatted number range to an Appendable. + * + * @param appendable + * The Appendable to which to append the formatted number range string. + * @param status + * Set if an error occurs while formatting the number range to the Appendable. + * @return The same Appendable, for chaining. + * @draft ICU 63 + * @see Appendable + */ + Appendable &appendTo(Appendable &appendable, UErrorCode& status) const; + + /** + * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given + * field in the output string. This allows you to determine the locations of, for example, + * the integer part, fraction part, or symbols. + * + * If both sides of the range have the same field, the field will occur twice, once before the + * range separator and once after the range separator, if applicable. + * + * If a field occurs just once, calling this method will find that occurrence and return it. If a + * field occurs multiple times, this method may be called repeatedly with the following pattern: + * + *

+     * FieldPosition fpos(UNUM_INTEGER_FIELD);
+     * while (formattedNumberRange.nextFieldPosition(fpos, status)) {
+     *   // do something with fpos.
+     * }
+     * 
+ * + * This method is useful if you know which field to query. If you want all available field position + * information, use #getAllFieldPositions(). + * + * @param fieldPosition + * Input+output variable. See {@link FormattedNumber#nextFieldPosition}. + * @param status + * Set if an error occurs while populating the FieldPosition. + * @return TRUE if a new occurrence of the field was found; FALSE otherwise. + * @draft ICU 63 + * @see UNumberFormatFields + */ + UBool nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const; + + /** + * Export the formatted number range to a FieldPositionIterator. This allows you to determine which characters in + * the output string correspond to which fields, such as the integer part, fraction part, and sign. + * + * If information on only one field is needed, use #nextFieldPosition() instead. + * + * @param iterator + * The FieldPositionIterator to populate with all of the fields present in the formatted number. + * @param status + * Set if an error occurs while populating the FieldPositionIterator. + * @draft ICU 63 + * @see UNumberFormatFields + */ + void getAllFieldPositions(FieldPositionIterator &iterator, UErrorCode &status) const; + + /** + * Export the first formatted number as a decimal number. This endpoint + * is useful for obtaining the exact number being printed after scaling + * and rounding have been applied by the number range formatting pipeline. + * + * The syntax of the unformatted number is a "numeric string" + * as defined in the Decimal Arithmetic Specification, available at + * http://speleotrove.com/decimal + * + * @return A decimal representation of the first formatted number. + * @draft ICU 63 + * @see NumberRangeFormatter + * @see #getSecondDecimal + */ + UnicodeString getFirstDecimal(UErrorCode& status) const; + + /** + * Export the second formatted number as a decimal number. This endpoint + * is useful for obtaining the exact number being printed after scaling + * and rounding have been applied by the number range formatting pipeline. + * + * The syntax of the unformatted number is a "numeric string" + * as defined in the Decimal Arithmetic Specification, available at + * http://speleotrove.com/decimal + * + * @return A decimal representation of the second formatted number. + * @draft ICU 63 + * @see NumberRangeFormatter + * @see #getFirstDecimal + */ + UnicodeString getSecondDecimal(UErrorCode& status) const; + + /** + * Returns whether the pair of numbers was successfully formatted as a range or whether an identity fallback was + * used. For example, if the first and second number were the same either before or after rounding occurred, an + * identity fallback was used. + * + * @return An indication the resulting identity situation in the formatted number range. + * @draft ICU 63 + * @see UNumberRangeIdentityFallback + */ + UNumberRangeIdentityResult getIdentityResult(UErrorCode& status) const; + + /** + * Copying not supported; use move constructor instead. + */ + FormattedNumberRange(const FormattedNumberRange&) = delete; + + /** + * Copying not supported; use move assignment instead. + */ + FormattedNumberRange& operator=(const FormattedNumberRange&) = delete; + + /** + * Move constructor: + * Leaves the source FormattedNumberRange in an undefined state. + * @draft ICU 63 + */ + FormattedNumberRange(FormattedNumberRange&& src) U_NOEXCEPT; + + /** + * Move assignment: + * Leaves the source FormattedNumberRange in an undefined state. + * @draft ICU 63 + */ + FormattedNumberRange& operator=(FormattedNumberRange&& src) U_NOEXCEPT; + + /** + * Destruct an instance of FormattedNumberRange, cleaning up any memory it might own. + * @draft ICU 63 + */ + ~FormattedNumberRange(); + + private: + // Can't use LocalPointer because UFormattedNumberRangeData is forward-declared + const impl::UFormattedNumberRangeData *fResults; + + // Error code for the terminal methods + UErrorCode fErrorCode; + + /** + * Internal constructor from data type. Adopts the data pointer. + * @internal + */ + explicit FormattedNumberRange(impl::UFormattedNumberRangeData *results) + : fResults(results), fErrorCode(U_ZERO_ERROR) {}; + + explicit FormattedNumberRange(UErrorCode errorCode) + : fResults(nullptr), fErrorCode(errorCode) {}; + + void getAllFieldPositionsImpl(FieldPositionIteratorHandler& fpih, UErrorCode& status) const; + + // To give LocalizedNumberRangeFormatter format methods access to this class's constructor: + friend class LocalizedNumberRangeFormatter; +}; + +/** + * See the main description in numberrangeformatter.h for documentation and examples. + * + * @draft ICU 63 + */ +class U_I18N_API NumberRangeFormatter final { + public: + /** + * Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is not currently + * known at the call site. + * + * @return An {@link UnlocalizedNumberRangeFormatter}, to be used for chaining. + * @draft ICU 63 + */ + static UnlocalizedNumberRangeFormatter with(); + + /** + * Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call + * site. + * + * @param locale + * The locale from which to load formats and symbols for number range formatting. + * @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining. + * @draft ICU 63 + */ + static LocalizedNumberRangeFormatter withLocale(const Locale &locale); + + /** + * Use factory methods instead of the constructor to create a NumberFormatter. + */ + NumberRangeFormatter() = delete; +}; + +} // namespace number +U_NAMESPACE_END + +#endif // U_HIDE_DRAFT_API + +#endif // __NUMBERRANGEFORMATTER_H__ + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/Makefile.in b/icu4c/source/test/intltest/Makefile.in index 4d3e17435ea..ad47ad0a14b 100644 --- a/icu4c/source/test/intltest/Makefile.in +++ b/icu4c/source/test/intltest/Makefile.in @@ -66,7 +66,7 @@ numbertest_affixutils.o numbertest_api.o numbertest_decimalquantity.o \ numbertest_modifiers.o numbertest_patternmodifier.o numbertest_patternstring.o \ numbertest_stringbuilder.o numbertest_stringsegment.o \ numbertest_parse.o numbertest_doubleconversion.o numbertest_skeletons.o \ -static_unisets_test.o numfmtdatadriventest.o erarulestest.o +static_unisets_test.o numfmtdatadriventest.o numbertest_range.o erarulestest.o DEPS = $(OBJECTS:.o=.d) diff --git a/icu4c/source/test/intltest/intltest.vcxproj b/icu4c/source/test/intltest/intltest.vcxproj index 60d34a0c957..50e0e2e89c2 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj +++ b/icu4c/source/test/intltest/intltest.vcxproj @@ -256,6 +256,7 @@ + diff --git a/icu4c/source/test/intltest/intltest.vcxproj.filters b/icu4c/source/test/intltest/intltest.vcxproj.filters index fc2ecb53050..f1b740dbe99 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj.filters +++ b/icu4c/source/test/intltest/intltest.vcxproj.filters @@ -289,6 +289,9 @@ formatting + + formatting + formatting diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 8984fdc3669..0d0f7dec815 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -11,6 +11,8 @@ #include "number_affixutils.h" #include "numparse_stringsegment.h" #include "unicode/locid.h" +#include "unicode/numberformatter.h" +#include "unicode/numberrangeformatter.h" using namespace icu::number; using namespace icu::number::impl; @@ -244,6 +246,53 @@ class NumberSkeletonTest : public IntlTest { void expectedErrorSkeleton(const char16_t** cases, int32_t casesLen); }; +class NumberRangeFormatterTest : public IntlTest { + public: + NumberRangeFormatterTest(); + NumberRangeFormatterTest(UErrorCode &status); + + void testSanity(); + void testBasic(); + void testCollapse(); + void testIdentity(); + void testDifferentFormatters(); + void testPlurals(); + + void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0); + + private: + CurrencyUnit USD; + CurrencyUnit GBP; + CurrencyUnit PTE; + + MeasureUnit METER; + MeasureUnit KILOMETER; + MeasureUnit FAHRENHEIT; + MeasureUnit KELVIN; + + void assertFormatRange( + const char16_t* message, + const UnlocalizedNumberRangeFormatter& f, + Locale locale, + const char16_t* expected_10_50, + const char16_t* expected_49_51, + const char16_t* expected_50_50, + const char16_t* expected_00_30, + const char16_t* expected_00_00, + const char16_t* expected_30_3K, + const char16_t* expected_30K_50K, + const char16_t* expected_49K_51K, + const char16_t* expected_50K_50K, + const char16_t* expected_50K_50M); + + void assertFormattedRangeEquals( + const char16_t* message, + const LocalizedNumberRangeFormatter& l, + double first, + double second, + const char16_t* expected); +}; + // NOTE: This macro is identical to the one in itformat.cpp #define TESTCLASS(id, TestClass) \ @@ -276,6 +325,7 @@ class NumberTest : public IntlTest { TESTCLASS(8, StringSegmentTest); TESTCLASS(9, NumberParserTest); TESTCLASS(10, NumberSkeletonTest); + TESTCLASS(11, NumberRangeFormatterTest); default: name = ""; break; // needed to end loop } } diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 59c0d20b653..0604a9d9db1 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -17,6 +17,7 @@ #include "unicode/utypes.h" // Horrible workaround for the lack of a status code in the constructor... +// (Also affects numbertest_range.cpp) UErrorCode globalNumberFormatterApiTestStatus = U_ZERO_ERROR; NumberFormatterApiTest::NumberFormatterApiTest() diff --git a/icu4c/source/test/intltest/numbertest_modifiers.cpp b/icu4c/source/test/intltest/numbertest_modifiers.cpp index 6d8f1990af8..52f4f494751 100644 --- a/icu4c/source/test/intltest/numbertest_modifiers.cpp +++ b/icu4c/source/test/intltest/numbertest_modifiers.cpp @@ -162,12 +162,12 @@ void ModifiersTest::assertModifierEquals(const Modifier &mod, NumberStringBuilde UErrorCode &status) { int32_t oldCount = sb.codePointCount(); mod.apply(sb, 0, sb.length(), status); - assertEquals("Prefix length", expectedPrefixLength, mod.getPrefixLength(status)); + assertEquals("Prefix length", expectedPrefixLength, mod.getPrefixLength()); assertEquals("Strong", expectedStrong, mod.isStrong()); if (dynamic_cast(&mod) == nullptr) { // i.e., if mod is not a CurrencySpacingEnabledModifier assertEquals("Code point count equals actual code point count", - sb.codePointCount() - oldCount, mod.getCodePointCount(status)); + sb.codePointCount() - oldCount, mod.getCodePointCount()); } UnicodeString debugString; diff --git a/icu4c/source/test/intltest/numbertest_patternmodifier.cpp b/icu4c/source/test/intltest/numbertest_patternmodifier.cpp index e3071b371f7..3a0fda82675 100644 --- a/icu4c/source/test/intltest/numbertest_patternmodifier.cpp +++ b/icu4c/source/test/intltest/numbertest_patternmodifier.cpp @@ -170,14 +170,14 @@ void PatternModifierTest::testMutableEqualsImmutable() { UnicodeString PatternModifierTest::getPrefix(const MutablePatternModifier &mod, UErrorCode &status) { NumberStringBuilder nsb; mod.apply(nsb, 0, 0, status); - int32_t prefixLength = mod.getPrefixLength(status); + int32_t prefixLength = mod.getPrefixLength(); return UnicodeString(nsb.toUnicodeString(), 0, prefixLength); } UnicodeString PatternModifierTest::getSuffix(const MutablePatternModifier &mod, UErrorCode &status) { NumberStringBuilder nsb; mod.apply(nsb, 0, 0, status); - int32_t prefixLength = mod.getPrefixLength(status); + int32_t prefixLength = mod.getPrefixLength(); return UnicodeString(nsb.toUnicodeString(), prefixLength, nsb.length() - prefixLength); } diff --git a/icu4c/source/test/intltest/numbertest_range.cpp b/icu4c/source/test/intltest/numbertest_range.cpp new file mode 100644 index 00000000000..1199f886545 --- /dev/null +++ b/icu4c/source/test/intltest/numbertest_range.cpp @@ -0,0 +1,750 @@ +// © 2018 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 "numbertest.h" +#include "unicode/numberrangeformatter.h" + +#include +#include + +// Horrible workaround for the lack of a status code in the constructor... +// (Also affects numbertest_api.cpp) +UErrorCode globalNumberRangeFormatterTestStatus = U_ZERO_ERROR; + +NumberRangeFormatterTest::NumberRangeFormatterTest() + : NumberRangeFormatterTest(globalNumberRangeFormatterTestStatus) { +} + +NumberRangeFormatterTest::NumberRangeFormatterTest(UErrorCode& status) + : USD(u"USD", status), + GBP(u"GBP", status), + PTE(u"PTE", status) { + + // Check for error on the first MeasureUnit in case there is no data + LocalPointer unit(MeasureUnit::createMeter(status)); + if (U_FAILURE(status)) { + dataerrln("%s %d status = %s", __FILE__, __LINE__, u_errorName(status)); + return; + } + METER = *unit; + + KILOMETER = *LocalPointer(MeasureUnit::createKilometer(status)); + FAHRENHEIT = *LocalPointer(MeasureUnit::createFahrenheit(status)); + KELVIN = *LocalPointer(MeasureUnit::createKelvin(status)); +} + +void NumberRangeFormatterTest::runIndexedTest(int32_t index, UBool exec, const char*& name, char*) { + if (exec) { + logln("TestSuite NumberRangeFormatterTest: "); + } + TESTCASE_AUTO_BEGIN; + TESTCASE_AUTO(testSanity); + TESTCASE_AUTO(testBasic); + TESTCASE_AUTO(testCollapse); + TESTCASE_AUTO(testIdentity); + TESTCASE_AUTO(testDifferentFormatters); + TESTCASE_AUTO(testPlurals); + TESTCASE_AUTO_END; +} + +void NumberRangeFormatterTest::testSanity() { + IcuTestErrorCode status(*this, "testSanity"); + LocalizedNumberRangeFormatter lnrf1 = NumberRangeFormatter::withLocale("en-us"); + LocalizedNumberRangeFormatter lnrf2 = NumberRangeFormatter::with().locale("en-us"); + assertEquals("Formatters should have same behavior 1", + lnrf1.formatFormattableRange(4, 6, status).toString(status), + lnrf2.formatFormattableRange(4, 6, status).toString(status)); +} + +void NumberRangeFormatterTest::testBasic() { + assertFormatRange( + u"Basic", + NumberRangeFormatter::with(), + Locale("en-us"), + u"1–5", + u"~5", + u"~5", + u"0–3", + u"~0", + u"3–3,000", + u"3,000–5,000", + u"4,999–5,001", + u"~5,000", + u"5,000–5,000,000"); + + assertFormatRange( + u"Basic with units", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3,000 m", + u"3,000–5,000 m", + u"4,999–5,001 m", + u"~5,000 m", + u"5,000–5,000,000 m"); + + assertFormatRange( + u"Basic with different units", + NumberRangeFormatter::with() + .numberFormatterFirst(NumberFormatter::with().unit(METER)) + .numberFormatterSecond(NumberFormatter::with().unit(KILOMETER)), + Locale("en-us"), + u"1 m – 5 km", + u"5 m – 5 km", + u"5 m – 5 km", + u"0 m – 3 km", + u"0 m – 0 km", + u"3 m – 3,000 km", + u"3,000 m – 5,000 km", + u"4,999 m – 5,001 km", + u"5,000 m – 5,000 km", + u"5,000 m – 5,000,000 km"); + + assertFormatRange( + u"Basic long unit", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(METER).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)), + Locale("en-us"), + u"1–5 meters", + u"~5 meters", + u"~5 meters", + u"0–3 meters", + u"~0 meters", + u"3–3,000 meters", + u"3,000–5,000 meters", + u"4,999–5,001 meters", + u"~5,000 meters", + u"5,000–5,000,000 meters"); + + assertFormatRange( + u"Non-English locale and unit", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)), + Locale("fr-FR"), + u"1–5 degrés Fahrenheit", + u"~5 degrés Fahrenheit", + u"~5 degrés Fahrenheit", + u"0–3 degrés Fahrenheit", + u"~0 degré Fahrenheit", + u"3–3 000 degrés Fahrenheit", + u"3 000–5 000 degrés Fahrenheit", + u"4 999–5 001 degrés Fahrenheit", + u"~5 000 degrés Fahrenheit", + u"5 000–5 000 000 degrés Fahrenheit"); + + assertFormatRange( + u"Locale with custom range separator", + NumberRangeFormatter::with(), + Locale("ja"), + u"1~5", + u"~5", + u"~5", + u"0~3", + u"~0", + u"3~3,000", + u"3,000~5,000", + u"4,999~5,001", + u"~5,000", + u"5,000~5,000,000"); + + assertFormatRange( + u"Locale that already has spaces around range separator", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with().unit(KELVIN)), + Locale("hr"), + u"1 K – 5 K", + u"~5 K", + u"~5 K", + u"0 K – 3 K", + u"~0 K", + u"3 K – 3.000 K", + u"3.000 K – 5.000 K", + u"4.999 K – 5.001 K", + u"~5.000 K", + u"5.000 K – 5.000.000 K"); + + assertFormatRange( + u"Locale with custom numbering system and no plural ranges data", + NumberRangeFormatter::with(), + Locale("shn@numbers=beng"), + // 012459 = ০১৩৪৫৯ + u"১–৫", + u"~৫", + u"~৫", + u"০–৩", + u"~০", + u"৩–৩,০০০", + u"৩,০০০–৫,০০০", + u"৪,৯৯৯–৫,০০১", + u"~৫,০০০", + u"৫,০০০–৫,০০০,০০০"); + + assertFormatRange( + u"Portuguese currency", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(PTE)), + Locale("pt-PT"), + u"1$00 - 5$00 \u200B", + u"~5$00 \u200B", + u"~5$00 \u200B", + u"0$00 - 3$00 \u200B", + u"~0$00 \u200B", + u"3$00 - 3000$00 \u200B", + u"3000$00 - 5000$00 \u200B", + u"4999$00 - 5001$00 \u200B", + u"~5000$00 \u200B", + u"5000$00 - 5,000,000$00 \u200B"); +} + +void NumberRangeFormatterTest::testCollapse() { + assertFormatRange( + u"Default collapse on currency (default rounding)", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(USD)), + Locale("en-us"), + u"$1.00 – $5.00", + u"~$5.00", + u"~$5.00", + u"$0.00 – $3.00", + u"~$0.00", + u"$3.00 – $3,000.00", + u"$3,000.00 – $5,000.00", + u"$4,999.00 – $5,001.00", + u"~$5,000.00", + u"$5,000.00 – $5,000,000.00"); + + assertFormatRange( + u"Default collapse on currency", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())), + Locale("en-us"), + u"$1 – $5", + u"~$5", + u"~$5", + u"$0 – $3", + u"~$0", + u"$3 – $3,000", + u"$3,000 – $5,000", + u"$4,999 – $5,001", + u"~$5,000", + u"$5,000 – $5,000,000"); + + assertFormatRange( + u"No collapse on currency", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())), + Locale("en-us"), + u"$1 – $5", + u"~$5", + u"~$5", + u"$0 – $3", + u"~$0", + u"$3 – $3,000", + u"$3,000 – $5,000", + u"$4,999 – $5,001", + u"~$5,000", + u"$5,000 – $5,000,000"); + + assertFormatRange( + u"Unit collapse on currency", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_UNIT) + .numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())), + Locale("en-us"), + u"$1–5", + u"~$5", + u"~$5", + u"$0–3", + u"~$0", + u"$3–3,000", + u"$3,000–5,000", + u"$4,999–5,001", + u"~$5,000", + u"$5,000–5,000,000"); + + assertFormatRange( + u"All collapse on currency", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_ALL) + .numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())), + Locale("en-us"), + u"$1–5", + u"~$5", + u"~$5", + u"$0–3", + u"~$0", + u"$3–3,000", + u"$3,000–5,000", + u"$4,999–5,001", + u"~$5,000", + u"$5,000–5,000,000"); + + assertFormatRange( + u"Default collapse on currency ISO code", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with() + .unit(GBP) + .unitWidth(UNUM_UNIT_WIDTH_ISO_CODE) + .precision(Precision::integer())), + Locale("en-us"), + u"GBP 1–5", + u"~GBP 5", // TODO: Fix this at some point + u"~GBP 5", + u"GBP 0–3", + u"~GBP 0", + u"GBP 3–3,000", + u"GBP 3,000–5,000", + u"GBP 4,999–5,001", + u"~GBP 5,000", + u"GBP 5,000–5,000,000"); + + assertFormatRange( + u"No collapse on currency ISO code", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with() + .unit(GBP) + .unitWidth(UNUM_UNIT_WIDTH_ISO_CODE) + .precision(Precision::integer())), + Locale("en-us"), + u"GBP 1 – GBP 5", + u"~GBP 5", // TODO: Fix this at some point + u"~GBP 5", + u"GBP 0 – GBP 3", + u"~GBP 0", + u"GBP 3 – GBP 3,000", + u"GBP 3,000 – GBP 5,000", + u"GBP 4,999 – GBP 5,001", + u"~GBP 5,000", + u"GBP 5,000 – GBP 5,000,000"); + + assertFormatRange( + u"Unit collapse on currency ISO code", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_UNIT) + .numberFormatterBoth(NumberFormatter::with() + .unit(GBP) + .unitWidth(UNUM_UNIT_WIDTH_ISO_CODE) + .precision(Precision::integer())), + Locale("en-us"), + u"GBP 1–5", + u"~GBP 5", // TODO: Fix this at some point + u"~GBP 5", + u"GBP 0–3", + u"~GBP 0", + u"GBP 3–3,000", + u"GBP 3,000–5,000", + u"GBP 4,999–5,001", + u"~GBP 5,000", + u"GBP 5,000–5,000,000"); + + assertFormatRange( + u"All collapse on currency ISO code", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_ALL) + .numberFormatterBoth(NumberFormatter::with() + .unit(GBP) + .unitWidth(UNUM_UNIT_WIDTH_ISO_CODE) + .precision(Precision::integer())), + Locale("en-us"), + u"GBP 1–5", + u"~GBP 5", // TODO: Fix this at some point + u"~GBP 5", + u"GBP 0–3", + u"~GBP 0", + u"GBP 3–3,000", + u"GBP 3,000–5,000", + u"GBP 4,999–5,001", + u"~GBP 5,000", + u"GBP 5,000–5,000,000"); + + // Default collapse on measurement unit is in testBasic() + + assertFormatRange( + u"No collapse on measurement unit", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with().unit(METER)), + Locale("en-us"), + u"1 m – 5 m", + u"~5 m", + u"~5 m", + u"0 m – 3 m", + u"~0 m", + u"3 m – 3,000 m", + u"3,000 m – 5,000 m", + u"4,999 m – 5,001 m", + u"~5,000 m", + u"5,000 m – 5,000,000 m"); + + assertFormatRange( + u"Unit collapse on measurement unit", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_UNIT) + .numberFormatterBoth(NumberFormatter::with().unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3,000 m", + u"3,000–5,000 m", + u"4,999–5,001 m", + u"~5,000 m", + u"5,000–5,000,000 m"); + + assertFormatRange( + u"All collapse on measurement unit", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_ALL) + .numberFormatterBoth(NumberFormatter::with().unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3,000 m", + u"3,000–5,000 m", + u"4,999–5,001 m", + u"~5,000 m", + u"5,000–5,000,000 m"); + + assertFormatRange( + u"Default collapse, long-form compact notation", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactLong())), + Locale("de-CH"), + u"1–5", + u"~5", + u"~5", + u"0–3", + u"~0", + u"3–3 Tausend", + u"3–5 Tausend", + u"~5 Tausend", + u"~5 Tausend", + u"5 Tausend – 5 Millionen"); + + assertFormatRange( + u"Unit collapse, long-form compact notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_UNIT) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactLong())), + Locale("de-CH"), + u"1–5", + u"~5", + u"~5", + u"0–3", + u"~0", + u"3–3 Tausend", + u"3 Tausend – 5 Tausend", + u"~5 Tausend", + u"~5 Tausend", + u"5 Tausend – 5 Millionen"); + + assertFormatRange( + u"Default collapse on measurement unit with compact-short notation", + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3K m", + u"3K – 5K m", + u"~5K m", + u"~5K m", + u"5K – 5M m"); + + assertFormatRange( + u"No collapse on measurement unit with compact-short notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)), + Locale("en-us"), + u"1 m – 5 m", + u"~5 m", + u"~5 m", + u"0 m – 3 m", + u"~0 m", + u"3 m – 3K m", + u"3K m – 5K m", + u"~5K m", + u"~5K m", + u"5K m – 5M m"); + + assertFormatRange( + u"Unit collapse on measurement unit with compact-short notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_UNIT) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3K m", + u"3K – 5K m", + u"~5K m", + u"~5K m", + u"5K – 5M m"); + + assertFormatRange( + u"All collapse on measurement unit with compact-short notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_ALL) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)), + Locale("en-us"), + u"1–5 m", + u"~5 m", + u"~5 m", + u"0–3 m", + u"~0 m", + u"3–3K m", + u"3–5K m", // this one is the key use case for ALL + u"~5K m", + u"~5K m", + u"5K – 5M m"); + + assertFormatRange( + u"No collapse on scientific notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_NONE) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())), + Locale("en-us"), + u"1E0 – 5E0", + u"~5E0", + u"~5E0", + u"0E0 – 3E0", + u"~0E0", + u"3E0 – 3E3", + u"3E3 – 5E3", + u"4.999E3 – 5.001E3", + u"~5E3", + u"5E3 – 5E6"); + + assertFormatRange( + u"All collapse on scientific notation", + NumberRangeFormatter::with() + .collapse(UNUM_RANGE_COLLAPSE_ALL) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())), + Locale("en-us"), + u"1–5E0", + u"~5E0", + u"~5E0", + u"0–3E0", + u"~0E0", + u"3E0 – 3E3", + u"3–5E3", + u"4.999–5.001E3", + u"~5E3", + u"5E3 – 5E6"); + + // TODO: Test compact currency? + // The code is not smart enough to differentiate the notation from the unit. +} + +void NumberRangeFormatterTest::testIdentity() { + assertFormatRange( + u"Identity fallback Range", + NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_RANGE), + Locale("en-us"), + u"1–5", + u"5–5", + u"5–5", + u"0–3", + u"0–0", + u"3–3,000", + u"3,000–5,000", + u"4,999–5,001", + u"5,000–5,000", + u"5,000–5,000,000"); + + assertFormatRange( + u"Identity fallback Approximately or Single Value", + NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE), + Locale("en-us"), + u"1–5", + u"~5", + u"5", + u"0–3", + u"0", + u"3–3,000", + u"3,000–5,000", + u"4,999–5,001", + u"5,000", + u"5,000–5,000,000"); + + assertFormatRange( + u"Identity fallback Single Value", + NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE), + Locale("en-us"), + u"1–5", + u"5", + u"5", + u"0–3", + u"0", + u"3–3,000", + u"3,000–5,000", + u"4,999–5,001", + u"5,000", + u"5,000–5,000,000"); + + assertFormatRange( + u"Identity fallback Approximately or Single Value with compact notation", + NumberRangeFormatter::with() + .identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE) + .numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort())), + Locale("en-us"), + u"1–5", + u"~5", + u"5", + u"0–3", + u"0", + u"3–3K", + u"3K – 5K", + u"~5K", + u"5K", + u"5K – 5M"); +} + +void NumberRangeFormatterTest::testDifferentFormatters() { + assertFormatRange( + u"Different rounding rules", + NumberRangeFormatter::with() + .numberFormatterFirst(NumberFormatter::with().precision(Precision::integer())) + .numberFormatterSecond(NumberFormatter::with().precision(Precision::fixedDigits(2))), + Locale("en-us"), + u"1–5.0", + u"5–5.0", + u"5–5.0", + u"0–3.0", + u"0–0.0", + u"3–3,000", + u"3,000–5,000", + u"4,999–5,000", + u"5,000–5,000", // TODO: Should this one be ~5,000? + u"5,000–5,000,000"); +} + +void NumberRangeFormatterTest::testPlurals() { + IcuTestErrorCode status(*this, "testPlurals"); + + // Locale sl has interesting plural forms: + // GBP{ + // one{"britanski funt"} + // two{"britanska funta"} + // few{"britanski funti"} + // other{"britanskih funtov"} + // } + Locale locale("sl"); + + UnlocalizedNumberFormatter unf = NumberFormatter::with() + .unit(GBP) + .unitWidth(UNUM_UNIT_WIDTH_FULL_NAME) + .precision(Precision::integer()); + LocalizedNumberFormatter lnf = unf.locale(locale); + + // For comparison, run the non-range version of the formatter + assertEquals(Int64ToUnicodeString(1), u"1 britanski funt", lnf.formatDouble(1, status).toString(status)); + assertEquals(Int64ToUnicodeString(2), u"2 britanska funta", lnf.formatDouble(2, status).toString(status)); + assertEquals(Int64ToUnicodeString(3), u"3 britanski funti", lnf.formatDouble(3, status).toString(status)); + assertEquals(Int64ToUnicodeString(5), u"5 britanskih funtov", lnf.formatDouble(5, status).toString(status)); + if (status.errIfFailureAndReset()) { return; } + + LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::with() + .numberFormatterBoth(unf) + .identityFallback(UNUM_IDENTITY_FALLBACK_RANGE) + .locale(locale); + + struct TestCase { + double first; + double second; + const char16_t* expected; + } cases[] = { + {1, 1, u"1–1 britanski funti"}, // one + one -> few + {1, 2, u"1–2 britanska funta"}, // one + two -> two + {1, 3, u"1–3 britanski funti"}, // one + few -> few + {1, 5, u"1–5 britanskih funtov"}, // one + other -> other + {2, 1, u"2–1 britanski funti"}, // two + one -> few + {2, 2, u"2–2 britanska funta"}, // two + two -> two + {2, 3, u"2–3 britanski funti"}, // two + few -> few + {2, 5, u"2–5 britanskih funtov"}, // two + other -> other + {3, 1, u"3–1 britanski funti"}, // few + one -> few + {3, 2, u"3–2 britanska funta"}, // few + two -> two + {3, 3, u"3–3 britanski funti"}, // few + few -> few + {3, 5, u"3–5 britanskih funtov"}, // few + other -> other + {5, 1, u"5–1 britanski funti"}, // other + one -> few + {5, 2, u"5–2 britanska funta"}, // other + two -> two + {5, 3, u"5–3 britanski funti"}, // other + few -> few + {5, 5, u"5–5 britanskih funtov"}, // other + other -> other + }; + for (auto& cas : cases) { + UnicodeString message = Int64ToUnicodeString(cas.first); + message += u" "; + message += Int64ToUnicodeString(cas.second); + status.setScope(message); + UnicodeString actual = lnrf.formatFormattableRange(cas.first, cas.second, status).toString(status); + assertEquals(message, cas.expected, actual); + status.errIfFailureAndReset(); + } +} + +void NumberRangeFormatterTest::assertFormatRange( + const char16_t* message, + const UnlocalizedNumberRangeFormatter& f, + Locale locale, + const char16_t* expected_10_50, + const char16_t* expected_49_51, + const char16_t* expected_50_50, + const char16_t* expected_00_30, + const char16_t* expected_00_00, + const char16_t* expected_30_3K, + const char16_t* expected_30K_50K, + const char16_t* expected_49K_51K, + const char16_t* expected_50K_50K, + const char16_t* expected_50K_50M) { + LocalizedNumberRangeFormatter l = f.locale(locale); + assertFormattedRangeEquals(message, l, 1, 5, expected_10_50); + assertFormattedRangeEquals(message, l, 4.9999999, 5.0000001, expected_49_51); + assertFormattedRangeEquals(message, l, 5, 5, expected_50_50); + assertFormattedRangeEquals(message, l, 0, 3, expected_00_30); + assertFormattedRangeEquals(message, l, 0, 0, expected_00_00); + assertFormattedRangeEquals(message, l, 3, 3000, expected_30_3K); + assertFormattedRangeEquals(message, l, 3000, 5000, expected_30K_50K); + assertFormattedRangeEquals(message, l, 4999, 5001, expected_49K_51K); + assertFormattedRangeEquals(message, l, 5000, 5000, expected_50K_50K); + assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M); +} + +void NumberRangeFormatterTest::assertFormattedRangeEquals( + const char16_t* message, + const LocalizedNumberRangeFormatter& l, + double first, + double second, + const char16_t* expected) { + IcuTestErrorCode status(*this, "assertFormattedRangeEquals"); + UnicodeString fullMessage = UnicodeString(message) + u": " + DoubleToUnicodeString(first) + u", " + DoubleToUnicodeString(second); + status.setScope(fullMessage); + UnicodeString actual = l.formatFormattableRange(first, second, status).toString(status); + assertEquals(fullMessage, expected, actual); +} + + +#endif diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ParameterizedModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AdoptingModifierStore.java similarity index 80% rename from icu4j/main/classes/core/src/com/ibm/icu/impl/number/ParameterizedModifier.java rename to icu4j/main/classes/core/src/com/ibm/icu/impl/number/AdoptingModifierStore.java index a553be4f30e..7e3459d1967 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ParameterizedModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AdoptingModifierStore.java @@ -5,10 +5,11 @@ package com.ibm.icu.impl.number; import com.ibm.icu.impl.StandardPlural; /** - * A ParameterizedModifier by itself is NOT a Modifier. Rather, it wraps a data structure containing two - * or more Modifiers and returns the modifier appropriate for the current situation. + * This implementation of ModifierStore adopts references to Modifiers. + * + * (This is named "adopting" because in C++, this class takes ownership of the Modifiers.) */ -public class ParameterizedModifier { +public class AdoptingModifierStore implements ModifierStore { private final Modifier positive; private final Modifier zero; private final Modifier negative; @@ -21,7 +22,7 @@ public class ParameterizedModifier { *

* If this constructor is used, a plural form CANNOT be passed to {@link #getModifier}. */ - public ParameterizedModifier(Modifier positive, Modifier zero, Modifier negative) { + public AdoptingModifierStore(Modifier positive, Modifier zero, Modifier negative) { this.positive = positive; this.zero = zero; this.negative = negative; @@ -36,7 +37,7 @@ public class ParameterizedModifier { *

* If this constructor is used, a plural form MUST be passed to {@link #getModifier}. */ - public ParameterizedModifier() { + public AdoptingModifierStore() { this.positive = null; this.zero = null; this.negative = null; @@ -53,7 +54,7 @@ public class ParameterizedModifier { frozen = true; } - public Modifier getModifier(int signum) { + public Modifier getModifierWithoutPlural(int signum) { assert frozen; assert mods == null; return signum == 0 ? zero : signum < 0 ? negative : positive; @@ -66,6 +67,8 @@ public class ParameterizedModifier { } private static int getModIndex(int signum, StandardPlural plural) { + assert signum >= -1 && signum <= 1; + assert plural != null; return plural.ordinal() * 3 + (signum + 1); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java index 4ebb3d0cc63..fc881a2c504 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java @@ -74,6 +74,28 @@ public class ConstantAffixModifier implements Modifier { return strong; } + @Override + public boolean containsField(Field field) { + // This method is not currently used. + assert false; + return false; + } + + @Override + public Parameters getParameters() { + return null; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + if (!(other instanceof ConstantAffixModifier)) { + return false; + } + ConstantAffixModifier _other = (ConstantAffixModifier) other; + return prefix.equals(_other.prefix) && suffix.equals(_other.suffix) && field == _other.field + && strong == _other.strong; + } + @Override public String toString() { return String.format("", prefix, suffix); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java index cdd129c128f..d53349204be 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java @@ -2,6 +2,8 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import java.util.Arrays; + import com.ibm.icu.text.NumberFormat.Field; /** @@ -20,17 +22,30 @@ public class ConstantMultiFieldModifier implements Modifier { private final boolean overwrite; private final boolean strong; + // Parameters: used for number range formatting + private final Parameters parameters; + public ConstantMultiFieldModifier( NumberStringBuilder prefix, NumberStringBuilder suffix, boolean overwrite, boolean strong) { + this(prefix, suffix, overwrite, strong, null); + } + + public ConstantMultiFieldModifier( + NumberStringBuilder prefix, + NumberStringBuilder suffix, + boolean overwrite, + boolean strong, + Parameters parameters) { prefixChars = prefix.toCharArray(); suffixChars = suffix.toCharArray(); prefixFields = prefix.toFieldArray(); suffixFields = suffix.toFieldArray(); this.overwrite = overwrite; this.strong = strong; + this.parameters = parameters; } @Override @@ -59,6 +74,40 @@ public class ConstantMultiFieldModifier implements Modifier { return strong; } + @Override + public boolean containsField(Field field) { + for (int i = 0; i < prefixFields.length; i++) { + if (prefixFields[i] == field) { + return true; + } + } + for (int i = 0; i < suffixFields.length; i++) { + if (suffixFields[i] == field) { + return true; + } + } + return false; + } + + @Override + public Parameters getParameters() { + return parameters; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + if (!(other instanceof ConstantMultiFieldModifier)) { + return false; + } + ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other; + if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) { + return true; + } + return Arrays.equals(prefixChars, _other.prefixChars) && Arrays.equals(prefixFields, _other.prefixFields) + && Arrays.equals(suffixChars, _other.suffixChars) && Arrays.equals(suffixFields, _other.suffixFields) + && overwrite == _other.overwrite && strong == _other.strong; + } + @Override public String toString() { NumberStringBuilder temp = new NumberStringBuilder(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java index 00a64bbe2ff..22b13b4f6e3 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -1014,6 +1014,46 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } } + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof DecimalQuantity_AbstractBCD)) { + return false; + } + DecimalQuantity_AbstractBCD _other = (DecimalQuantity_AbstractBCD) other; + + boolean basicEquals = + scale == _other.scale + && precision == _other.precision + && flags == _other.flags + && lOptPos == _other.lOptPos + && lReqPos == _other.lReqPos + && rReqPos == _other.rReqPos + && rOptPos == _other.rOptPos + && isApproximate == _other.isApproximate; + if (!basicEquals) { + return false; + } + + if (precision == 0) { + return true; + } else if (isApproximate) { + return origDouble == _other.origDouble && origDelta == _other.origDelta; + } else { + for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) { + if (getDigit(m) != _other.getDigit(m)) { + return false; + } + } + return true; + } + } + /** * Returns a single digit from the BCD list. No internal state is changed by calling this method. * diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java index bb86ba1ef5b..61921390ac6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java @@ -21,7 +21,7 @@ import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; -public class LongNameHandler implements MicroPropsGenerator { +public class LongNameHandler implements MicroPropsGenerator, ModifierStore { private static final int DNAM_INDEX = StandardPlural.COUNT; private static final int PER_INDEX = StandardPlural.COUNT + 1; @@ -175,10 +175,11 @@ public class LongNameHandler implements MicroPropsGenerator { String[] simpleFormats = new String[ARRAY_LENGTH]; getCurrencyLongNameData(locale, currency, simpleFormats); // TODO(ICU4J): Reduce the number of object creations here? - Map modifiers = new EnumMap( + Map modifiers = new EnumMap<>( StandardPlural.class); - simpleFormatsToModifiers(simpleFormats, null, modifiers); - return new LongNameHandler(modifiers, rules, parent); + LongNameHandler result = new LongNameHandler(modifiers, rules, parent); + result.simpleFormatsToModifiers(simpleFormats, null); + return result; } public static LongNameHandler forMeasureUnit( @@ -203,10 +204,11 @@ public class LongNameHandler implements MicroPropsGenerator { getMeasureData(locale, unit, width, simpleFormats); // TODO: What field to use for units? // TODO(ICU4J): Reduce the number of object creations here? - Map modifiers = new EnumMap( + Map modifiers = new EnumMap<>( StandardPlural.class); - simpleFormatsToModifiers(simpleFormats, null, modifiers); - return new LongNameHandler(modifiers, rules, parent); + LongNameHandler result = new LongNameHandler(modifiers, rules, parent); + result.simpleFormatsToModifiers(simpleFormats, null); + return result; } private static LongNameHandler forCompoundUnit( @@ -238,29 +240,32 @@ public class LongNameHandler implements MicroPropsGenerator { perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString); } // TODO: What field to use for units? - Map modifiers = new EnumMap( + Map modifiers = new EnumMap<>( StandardPlural.class); - multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null, modifiers); - return new LongNameHandler(modifiers, rules, parent); + LongNameHandler result = new LongNameHandler(modifiers, rules, parent); + result.multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null); + return result; } - private static void simpleFormatsToModifiers( + private void simpleFormatsToModifiers( String[] simpleFormats, - NumberFormat.Field field, - Map output) { + NumberFormat.Field field) { StringBuilder sb = new StringBuilder(); for (StandardPlural plural : StandardPlural.VALUES) { String simpleFormat = getWithPlural(simpleFormats, plural); String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 0, 1); - output.put(plural, new SimpleModifier(compiled, field, false)); + Modifier.Parameters parameters = new Modifier.Parameters(); + parameters.obj = this; + parameters.signum = 0; + parameters.plural = plural; + modifiers.put(plural, new SimpleModifier(compiled, field, false, parameters)); } } - private static void multiSimpleFormatsToModifiers( + private void multiSimpleFormatsToModifiers( String[] leadFormats, String trailFormat, - NumberFormat.Field field, - Map output) { + NumberFormat.Field field) { StringBuilder sb = new StringBuilder(); String trailCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(trailFormat, sb, 1, 1); for (StandardPlural plural : StandardPlural.VALUES) { @@ -268,7 +273,11 @@ public class LongNameHandler implements MicroPropsGenerator { String compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat); String compoundCompiled = SimpleFormatterImpl .compileToStringMinMaxArguments(compoundFormat, sb, 0, 1); - output.put(plural, new SimpleModifier(compoundCompiled, field, false)); + Modifier.Parameters parameters = new Modifier.Parameters(); + parameters.obj = this; + parameters.signum = 0; + parameters.plural = plural; + modifiers.put(plural, new SimpleModifier(compoundCompiled, field, false, parameters)); } } @@ -281,4 +290,9 @@ public class LongNameHandler implements MicroPropsGenerator { micros.modOuter = modifiers.get(copy.getStandardPlural(rules)); return micros; } + + @Override + public Modifier getModifier(int signum, StandardPlural plural) { + return modifiers.get(plural); + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java index a7ab9b98a89..d0e74bbe093 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java @@ -2,6 +2,9 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.text.NumberFormat.Field; + /** * A Modifier is an object that can be passed through the formatting pipeline until it is finally applied * to the string builder. A Modifier usually contains a prefix and a suffix that are applied, but it @@ -48,4 +51,30 @@ public interface Modifier { * @return Whether the modifier is strong. */ public boolean isStrong(); + + /** + * Whether the modifier contains at least one occurrence of the given field. + */ + public boolean containsField(Field currency); + + /** + * A fill-in for getParameters(). obj will always be set; if non-null, the other + * two fields are also safe to read. + */ + public static class Parameters { + public ModifierStore obj; + public int signum; + public StandardPlural plural; + } + + /** + * Gets a set of "parameters" for this Modifier. + */ + public Parameters getParameters(); + + /** + * Returns whether this Modifier is *semantically equivalent* to the other Modifier; + * in many cases, this is the same as equal, but parameters should be ignored. + */ + public boolean semanticallyEquivalent(Modifier other); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java new file mode 100644 index 00000000000..1751c1cb869 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierStore.java @@ -0,0 +1,18 @@ +// © 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 com.ibm.icu.impl.StandardPlural; + +/** + * This is *not* a modifier; rather, it is an object that can return modifiers + * based on given parameters. + * + * @author sffc + */ +public interface ModifierStore { + /** + * Returns a Modifier with the given parameters (best-effort). + */ + Modifier getModifier(int signum, StandardPlural plural); +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java index 13d0ab2de10..a651b3907d9 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java @@ -7,6 +7,7 @@ import com.ibm.icu.impl.number.AffixUtils.SymbolProvider; import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat.Field; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.Currency; @@ -165,7 +166,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr NumberStringBuilder b = new NumberStringBuilder(); if (needsPlurals()) { // Slower path when we require the plural keyword. - ParameterizedModifier pm = new ParameterizedModifier(); + AdoptingModifierStore pm = new AdoptingModifierStore(); for (StandardPlural plural : StandardPlural.VALUES) { setNumberProperties(1, plural); pm.setModifier(1, plural, createConstantModifier(a, b)); @@ -184,7 +185,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr Modifier zero = createConstantModifier(a, b); setNumberProperties(-1, null); Modifier negative = createConstantModifier(a, b); - ParameterizedModifier pm = new ParameterizedModifier(positive, zero, negative); + AdoptingModifierStore pm = new AdoptingModifierStore(positive, zero, negative); return new ImmutablePatternModifier(pm, null, parent); } } @@ -213,12 +214,12 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr } public static class ImmutablePatternModifier implements MicroPropsGenerator { - final ParameterizedModifier pm; + final AdoptingModifierStore pm; final PluralRules rules; final MicroPropsGenerator parent; ImmutablePatternModifier( - ParameterizedModifier pm, + AdoptingModifierStore pm, PluralRules rules, MicroPropsGenerator parent) { this.pm = pm; @@ -235,7 +236,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr public void applyToMicros(MicroProps micros, DecimalQuantity quantity) { if (rules == null) { - micros.modMiddle = pm.getModifier(quantity.signum()); + micros.modMiddle = pm.getModifierWithoutPlural(quantity.signum()); } else { // TODO: Fix this. Avoid the copy. DecimalQuantity copy = quantity.createCopy(); @@ -319,6 +320,27 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr return isStrong; } + @Override + public boolean containsField(Field field) { + // This method is not currently used. (unsafe path not used in range formatting) + assert false; + return false; + } + + @Override + public Parameters getParameters() { + // This method is not currently used. + assert false; + return null; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + // This method is not currently used. (unsafe path not used in range formatting) + assert false; + return false; + } + private int insertPrefix(NumberStringBuilder sb, int position) { prepareAffix(true); int length = AffixUtils.unescape(currentAffix, sb, position, this); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java index 5ac1bcfa408..30c12d61a82 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java @@ -3,7 +3,9 @@ package com.ibm.icu.impl.number; import com.ibm.icu.impl.SimpleFormatterImpl; +import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper; import com.ibm.icu.text.NumberFormat.Field; +import com.ibm.icu.util.ICUException; /** * The second primary implementation of {@link Modifier}, this one consuming a @@ -17,15 +19,24 @@ public class SimpleModifier implements Modifier { private final int suffixOffset; private final int suffixLength; + // Parameters: used for number range formatting + private final Parameters parameters; + /** TODO: This is copied from SimpleFormatterImpl. */ private static final int ARG_NUM_LIMIT = 0x100; /** Creates a modifier that uses the SimpleFormatter string formats. */ public SimpleModifier(String compiledPattern, Field field, boolean strong) { + this(compiledPattern, field, strong, null); + } + + /** Creates a modifier that uses the SimpleFormatter string formats. */ + public SimpleModifier(String compiledPattern, Field field, boolean strong, Parameters parameters) { assert compiledPattern != null; this.compiledPattern = compiledPattern; this.field = field; this.strong = strong; + this.parameters = parameters; int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern); if (argLimit == 0) { @@ -80,6 +91,30 @@ public class SimpleModifier implements Modifier { return strong; } + @Override + public boolean containsField(Field field) { + // This method is not currently used. + assert false; + return false; + } + + @Override + public Parameters getParameters() { + return parameters; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + if (!(other instanceof SimpleModifier)) { + return false; + } + SimpleModifier _other = (SimpleModifier) other; + if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) { + return true; + } + return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong; + } + /** * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not @@ -123,4 +158,66 @@ public class SimpleModifier implements Modifier { return prefixLength + suffixLength; } } + + /** + * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code. + * I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other. + * + *

+ * Applies the compiled two-argument pattern to the NumberStringBuilder. + * + *

+ * This method is optimized for the case where the prefix and suffix are often empty, such as + * in the range pattern like "{0}-{1}". + */ + public static void formatTwoArgPattern(String compiledPattern, NumberStringBuilder result, int index, PrefixInfixSuffixLengthHelper h, + Field field) { + int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern); + if (argLimit != 2) { + throw new ICUException(); + } + int offset = 1; // offset into compiledPattern + int length = 0; // chars added to result + + int prefixLength = compiledPattern.charAt(offset); + offset++; + if (prefixLength < ARG_NUM_LIMIT) { + // No prefix + prefixLength = 0; + } else { + prefixLength -= ARG_NUM_LIMIT; + result.insert(index + length, compiledPattern, offset, offset + prefixLength, field); + offset += prefixLength; + length += prefixLength; + offset++; + } + + int infixLength = compiledPattern.charAt(offset); + offset++; + if (infixLength < ARG_NUM_LIMIT) { + // No infix + infixLength = 0; + } else { + infixLength -= ARG_NUM_LIMIT; + result.insert(index + length, compiledPattern, offset, offset + infixLength, field); + offset += infixLength; + length += infixLength; + offset++; + } + + int suffixLength; + if (offset == compiledPattern.length()) { + // No suffix + suffixLength = 0; + } else { + suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT; + offset++; + result.insert(index + length, compiledPattern, offset, offset + suffixLength, field); + length += suffixLength; + } + + h.lengthPrefix = prefixLength; + h.lengthInfix = infixLength; + h.lengthSuffix = suffixLength; + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java new file mode 100644 index 00000000000..696f3f082c3 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java @@ -0,0 +1,30 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.range; + +/** + * A small, mutable internal helper class for keeping track of offsets on range patterns. + */ +public class PrefixInfixSuffixLengthHelper { + public int lengthPrefix = 0; + public int length1 = 0; + public int lengthInfix = 0; + public int length2 = 0; + public int lengthSuffix = 0; + + public int index0() { + return lengthPrefix; + } + + public int index1() { + return lengthPrefix + length1; + } + + public int index2() { + return lengthPrefix + length1 + lengthInfix; + } + + public int index3() { + return lengthPrefix + length1 + lengthInfix + length2; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java new file mode 100644 index 00000000000..e2872516a30 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/RangeMacroProps.java @@ -0,0 +1,48 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.range; + +import java.util.Objects; + +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.number.UnlocalizedNumberFormatter; +import com.ibm.icu.util.ULocale; + +/** + * @author sffc + * + */ +public class RangeMacroProps { + public UnlocalizedNumberFormatter formatter1; + public UnlocalizedNumberFormatter formatter2; + public int sameFormatters = -1; // -1 for unset, 0 for false, 1 for true + public RangeCollapse collapse; + public RangeIdentityFallback identityFallback; + public ULocale loc; + + @Override + public int hashCode() { + return Objects.hash(formatter1, + formatter2, + collapse, + identityFallback, + loc); + } + + @Override + public boolean equals(Object _other) { + if (_other == null) + return false; + if (this == _other) + return true; + if (!(_other instanceof RangeMacroProps)) + return false; + RangeMacroProps other = (RangeMacroProps) _other; + return Objects.equals(formatter1, other.formatter1) + && Objects.equals(formatter2, other.formatter2) + && Objects.equals(collapse, other.collapse) + && Objects.equals(identityFallback, other.identityFallback) + && Objects.equals(loc, other.loc); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java new file mode 100644 index 00000000000..8a9819a800c --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/range/StandardPluralRanges.java @@ -0,0 +1,104 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.range; + +import java.util.MissingResourceException; + +import com.ibm.icu.impl.ICUData; +import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.UResource; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.UResourceBundle; + +/** + * @author sffc + * + */ +public class StandardPluralRanges { + + StandardPlural[] flatTriples; + int numTriples = 0; + + //////////////////// + + private static final class PluralRangesDataSink extends UResource.Sink { + + StandardPluralRanges output; + + PluralRangesDataSink(StandardPluralRanges output) { + this.output = output; + } + + @Override + public void put(UResource.Key key, UResource.Value value, boolean noFallback) { + UResource.Array entriesArray = value.getArray(); + output.setCapacity(entriesArray.getSize()); + for (int i = 0; entriesArray.getValue(i, value); ++i) { + UResource.Array pluralFormsArray = value.getArray(); + pluralFormsArray.getValue(0, value); + StandardPlural first = StandardPlural.fromString(value.getString()); + pluralFormsArray.getValue(1, value); + StandardPlural second = StandardPlural.fromString(value.getString()); + pluralFormsArray.getValue(2, value); + StandardPlural result = StandardPlural.fromString(value.getString()); + output.addPluralRange(first, second, result); + } + } + } + + private static void getPluralRangesData( + ULocale locale, + StandardPluralRanges out) { + StringBuilder sb = new StringBuilder(); + ICUResourceBundle resource; + resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "pluralRanges"); + sb.append("locales/"); + sb.append(locale.getLanguage()); + String key = sb.toString(); + String set; + try { + set = resource.getStringWithFallback(key); + } catch (MissingResourceException e) { + // Not all languages are covered: fail gracefully + return; + } + + sb.setLength(0); + sb.append("rules/"); + sb.append(set); + key = sb.toString(); + PluralRangesDataSink sink = new PluralRangesDataSink(out); + resource.getAllItemsWithFallback(key, sink); + } + + //////////////////// + + public StandardPluralRanges(ULocale locale) { + getPluralRangesData(locale, this); + } + + /** Used for data loading. */ + private void addPluralRange(StandardPlural first, StandardPlural second, StandardPlural result) { + flatTriples[3 * numTriples] = first; + flatTriples[3 * numTriples + 1] = second; + flatTriples[3 * numTriples + 2] = result; + numTriples++; + } + + /** Used for data loading. */ + private void setCapacity(int length) { + flatTriples = new StandardPlural[length*3]; + } + + public StandardPlural resolve(StandardPlural first, StandardPlural second) { + for (int i = 0; i < numTriples; i++) { + if (first == flatTriples[3 * i] && second == flatTriples[3 * i + 1]) { + return flatTriples[3 * i + 2]; + } + } + // Default fallback + return StandardPlural.OTHER; + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java index 34030048ce4..9febde4358c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumber.java @@ -70,9 +70,9 @@ public class FormattedNumber { } /** - * Determine the start and end indices of the first occurrence of the given field in the - * output string. This allows you to determine the locations of the integer part, fraction part, and - * sign. + * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given + * field in the output string. This allows you to determine the locations of, for example, + * the integer part, fraction part, or symbols. * *

* If multiple different field attributes are needed, this method can be called repeatedly, or if diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java new file mode 100644 index 00000000000..ad1ab6b06ea --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java @@ -0,0 +1,209 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import java.io.IOException; +import java.math.BigDecimal; +import java.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.util.Arrays; + +import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult; +import com.ibm.icu.util.ICUUncheckedIOException; + +/** + * The result of a number range formatting operation. This class allows the result to be exported in several data types, + * including a String, an AttributedCharacterIterator, and a BigDecimal. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public class FormattedNumberRange { + final NumberStringBuilder string; + final DecimalQuantity quantity1; + final DecimalQuantity quantity2; + final RangeIdentityResult identityResult; + + FormattedNumberRange(NumberStringBuilder string, DecimalQuantity quantity1, DecimalQuantity quantity2, + RangeIdentityResult identityResult) { + this.string = string; + this.quantity1 = quantity1; + this.quantity2 = quantity2; + this.identityResult = identityResult; + } + + /** + * Creates a String representation of the the formatted number range. + * + * @return a String containing the localized number range. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + @Override + public String toString() { + return string.toString(); + } + + /** + * Append the formatted number range to an Appendable, such as a StringBuilder. This may be slightly more efficient + * than creating a String. + * + *

+ * If an IOException occurs when appending to the Appendable, an unchecked {@link ICUUncheckedIOException} is thrown + * instead. + * + * @param appendable + * The Appendable to which to append the formatted number range string. + * @return The same Appendable, for chaining. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see Appendable + * @see NumberRangeFormatter + */ + public A appendTo(A appendable) { + try { + appendable.append(string); + } catch (IOException e) { + // Throw as an unchecked exception to avoid users needing try/catch + throw new ICUUncheckedIOException(e); + } + return appendable; + } + + /** + * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given + * field in the output string. This allows you to determine the locations of, for example, + * the integer part, fraction part, or symbols. + *

+ * If both sides of the range have the same field, the field will occur twice, once before the range separator and + * once after the range separator, if applicable. + *

+ * If a field occurs just once, calling this method will find that occurrence and return it. If a field occurs + * multiple times, this method may be called repeatedly with the following pattern: + * + *

+     * FieldPosition fpos = new FieldPosition(NumberFormat.Field.INTEGER);
+     * while (formattedNumberRange.nextFieldPosition(fpos, status)) {
+     *     // do something with fpos.
+     * }
+     * 
+ *

+ * This method is useful if you know which field to query. If you want all available field position information, use + * {@link #toCharacterIterator()}. + * + * @param fieldPosition + * Input+output variable. See {@link FormattedNumber#nextFieldPosition(FieldPosition)}. + * @return true if a new occurrence of the field was found; false otherwise. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see com.ibm.icu.text.NumberFormat.Field + * @see NumberRangeFormatter + */ + public boolean nextFieldPosition(FieldPosition fieldPosition) { + return string.nextFieldPosition(fieldPosition); + } + + /** + * Export the formatted number range as an AttributedCharacterIterator. This allows you to determine which + * characters in the output string correspond to which fields, such as the integer part, fraction part, and + * sign. + *

+ * If information on only one field is needed, use {@link #nextFieldPosition(FieldPosition)} instead. + * + * @return An AttributedCharacterIterator, containing information on the field attributes of the number range + * string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see com.ibm.icu.text.NumberFormat.Field + * @see AttributedCharacterIterator + * @see NumberRangeFormatter + */ + public AttributedCharacterIterator toCharacterIterator() { + return string.toCharacterIterator(); + } + + /** + * Export the first formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being + * printed after scaling and rounding have been applied by the number range formatting pipeline. + * + * @return A BigDecimal representation of the first formatted number. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see #getSecondBigDecimal + */ + public BigDecimal getFirstBigDecimal() { + return quantity1.toBigDecimal(); + } + + /** + * Export the second formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being + * printed after scaling and rounding have been applied by the number range formatting pipeline. + * + * @return A BigDecimal representation of the second formatted number. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see #getFirstBigDecimal + */ + public BigDecimal getSecondBigDecimal() { + return quantity2.toBigDecimal(); + } + + /** + * Returns whether the pair of numbers was successfully formatted as a range or whether an identity fallback was + * used. For example, if the first and second number were the same either before or after rounding occurred, an + * identity fallback was used. + * + * @return A RangeIdentityType indicating the resulting identity situation in the formatted number range. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + * @see NumberRangeFormatter.RangeIdentityFallback + */ + public RangeIdentityResult getIdentityResult() { + return identityResult; + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int hashCode() { + // NumberStringBuilder and BigDecimal are mutable, so we can't call + // #equals() or #hashCode() on them directly. + return Arrays.hashCode(string.toCharArray()) ^ Arrays.hashCode(string.toFieldArray()) + ^ quantity1.toBigDecimal().hashCode() ^ quantity2.toBigDecimal().hashCode(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null) + return false; + if (!(other instanceof FormattedNumberRange)) + return false; + // NumberStringBuilder and BigDecimal are mutable, so we can't call + // #equals() or #hashCode() on them directly. + FormattedNumberRange _other = (FormattedNumberRange) other; + return Arrays.equals(string.toCharArray(), _other.string.toCharArray()) + && Arrays.equals(string.toFieldArray(), _other.string.toFieldArray()) + && quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal()) + && quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal()); + } +} 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 c877fb80089..29e9cb4ab75 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 @@ -152,9 +152,9 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings { + + private volatile NumberRangeFormatterImpl fImpl; + + LocalizedNumberRangeFormatter(NumberRangeFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + /** + * Format the given integers to a string using the settings specified in the NumberRangeFormatter fluent setting + * chain. + * + * @param first + * The first number in the range, usually to the left in LTR locales. + * @param second + * The second number in the range, usually to the right in LTR locales. + * @return A FormattedNumberRange object; call .toString() to get the string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public FormattedNumberRange formatRange(int first, int second) { + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + return formatImpl(dq1, dq2, first == second); + } + + /** + * Format the given doubles to a string using the settings specified in the NumberRangeFormatter fluent setting + * chain. + * + * @param first + * The first number in the range, usually to the left in LTR locales. + * @param second + * The second number in the range, usually to the right in LTR locales. + * @return A FormattedNumberRange object; call .toString() to get the string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public FormattedNumberRange formatRange(double first, double second) { + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + // Note: double equality could be changed to epsilon equality later if there is demand. + // The epsilon should be set via an API method. + return formatImpl(dq1, dq2, first == second); + } + + /** + * Format the given Numbers to a string using the settings specified in the NumberRangeFormatter fluent setting + * chain. + * + * @param first + * The first number in the range, usually to the left in LTR locales. + * @param second + * The second number in the range, usually to the right in LTR locales. + * @return A FormattedNumberRange object; call .toString() to get the string. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public FormattedNumberRange formatRange(Number first, Number second) { + if (first == null || second == null) { + throw new IllegalArgumentException("Cannot format null values in range"); + } + DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first); + DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second); + return formatImpl(dq1, dq2, first.equals(second)); + } + + FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second, boolean equalBeforeRounding) { + if (fImpl == null) { + fImpl = new NumberRangeFormatterImpl(resolve()); + } + return fImpl.format(first, second, equalBeforeRounding); + } + + @Override + LocalizedNumberRangeFormatter create(int key, Object value) { + return new LocalizedNumberRangeFormatter(this, key, value); + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java index 06dad539f33..11f590f56c4 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterImpl.java @@ -42,21 +42,21 @@ import com.ibm.icu.util.MeasureUnit; class NumberFormatterImpl { /** Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. */ - public static NumberFormatterImpl fromMacros(MacroProps macros) { - MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true); - return new NumberFormatterImpl(microPropsGenerator); + public NumberFormatterImpl(MacroProps macros) { + this(macrosToMicroGenerator(macros, true)); } /** * Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once. */ - public static void applyStatic( + public static int formatStatic( MacroProps macros, DecimalQuantity inValue, NumberStringBuilder outString) { - MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false); - MicroProps micros = microPropsGenerator.processQuantity(inValue); - microsToString(micros, inValue, outString); + MicroProps micros = preProcessUnsafe(macros, inValue); + int length = writeNumber(micros, inValue, outString, 0); + length += writeAffixes(micros, outString, 0, length); + return length; } /** @@ -82,9 +82,40 @@ class NumberFormatterImpl { this.microPropsGenerator = microPropsGenerator; } - public void apply(DecimalQuantity inValue, NumberStringBuilder outString) { + /** + * Evaluates the "safe" MicroPropsGenerator created by "fromMacros". + */ + public int format(DecimalQuantity inValue, NumberStringBuilder outString) { + MicroProps micros = preProcess(inValue); + int length = writeNumber(micros, inValue, outString, 0); + length += writeAffixes(micros, outString, 0, length); + return length; + } + + /** + * Like format(), but saves the result into an output MicroProps without additional processing. + */ + public MicroProps preProcess(DecimalQuantity inValue) { MicroProps micros = microPropsGenerator.processQuantity(inValue); - microsToString(micros, inValue, outString); + micros.rounder.apply(inValue); + if (micros.integerWidth.maxInt == -1) { + inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); + } else { + inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); + } + return micros; + } + + private static MicroProps preProcessUnsafe(MacroProps macros, DecimalQuantity inValue) { + MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false); + MicroProps micros = microPropsGenerator.processQuantity(inValue); + micros.rounder.apply(inValue); + if (micros.integerWidth.maxInt == -1) { + inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); + } else { + inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); + } + return micros; } public int getPrefixSuffix(byte signum, StandardPlural plural, NumberStringBuilder output) { @@ -350,64 +381,55 @@ class NumberFormatterImpl { ////////// /** - * Synthesizes the output string from a MicroProps and DecimalQuantity. - * - * @param micros - * The MicroProps after the quantity has been consumed. Will not be mutated. - * @param quantity - * The DecimalQuantity to be rendered. May be mutated. - * @param string - * The output string. Will be mutated. + * Adds the affixes. Intended to be called immediately after formatNumber. */ - private static void microsToString( + public static int writeAffixes( MicroProps micros, - DecimalQuantity quantity, - NumberStringBuilder string) { - micros.rounder.apply(quantity); - if (micros.integerWidth.maxInt == -1) { - quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); - } else { - quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); - } - int length = writeNumber(micros, quantity, string); - // NOTE: When range formatting is added, these modifiers can bubble up. - // For now, apply them all here at once. + NumberStringBuilder string, + int start, + int end) { // Always apply the inner modifier (which is "strong"). - length += micros.modInner.apply(string, 0, length); + int length = micros.modInner.apply(string, start, end); if (micros.padding.isValid()) { - micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, 0, length); + micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, start, end + length); } else { - length += micros.modMiddle.apply(string, 0, length); - length += micros.modOuter.apply(string, 0, length); + length += micros.modMiddle.apply(string, start, end + length); + length += micros.modOuter.apply(string, start, end + length); } + return length; } - private static int writeNumber( + /** + * Synthesizes the output string from a MicroProps and DecimalQuantity. + * This method formats only the main number, not affixes. + */ + public static int writeNumber( MicroProps micros, DecimalQuantity quantity, - NumberStringBuilder string) { + NumberStringBuilder string, + int index) { int length = 0; if (quantity.isInfinite()) { - length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER); + length += string.insert(length + index, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER); } else if (quantity.isNaN()) { - length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER); + length += string.insert(length + index, micros.symbols.getNaN(), NumberFormat.Field.INTEGER); } else { // Add the integer digits - length += writeIntegerDigits(micros, quantity, string); + length += writeIntegerDigits(micros, quantity, string, length + index); // Add the decimal point if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalSeparatorDisplay.ALWAYS) { - length += string.insert(length, + length += string.insert(length + index, micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString() : micros.symbols.getDecimalSeparatorString(), NumberFormat.Field.DECIMAL_SEPARATOR); } // Add the fraction digits - length += writeFractionDigits(micros, quantity, string); + length += writeFractionDigits(micros, quantity, string, length + index); } return length; @@ -416,13 +438,14 @@ class NumberFormatterImpl { private static int writeIntegerDigits( MicroProps micros, DecimalQuantity quantity, - NumberStringBuilder string) { + NumberStringBuilder string, + int index) { int length = 0; int integerCount = quantity.getUpperDisplayMagnitude() + 1; for (int i = 0; i < integerCount; i++) { // Add grouping separator if (micros.grouping.groupAtPosition(i, quantity)) { - length += string.insert(0, + length += string.insert(index, micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString() : micros.symbols.getGroupingSeparatorString(), NumberFormat.Field.GROUPING_SEPARATOR); @@ -431,11 +454,11 @@ class NumberFormatterImpl { // Get and append the next digit value byte nextDigit = quantity.getDigit(i); if (micros.symbols.getCodePointZero() != -1) { - length += string.insertCodePoint(0, + length += string.insertCodePoint(index, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.INTEGER); } else { - length += string.insert(0, + length += string.insert(index, micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.INTEGER); } @@ -446,17 +469,18 @@ class NumberFormatterImpl { private static int writeFractionDigits( MicroProps micros, DecimalQuantity quantity, - NumberStringBuilder string) { + NumberStringBuilder string, + int index) { int length = 0; int fractionCount = -quantity.getLowerDisplayMagnitude(); for (int i = 0; i < fractionCount; i++) { // Get and append the next digit value byte nextDigit = quantity.getDigit(-i - 1); if (micros.symbols.getCodePointZero() != -1) { - length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit, + length += string.insertCodePoint(length + index, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.FRACTION); } else { - length += string.append(micros.symbols.getDigitStringsLocal()[nextDigit], + length += string.insert(length + index, micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.FRACTION); } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java index 5867621c142..32b389a21ca 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatterSettings.java @@ -47,10 +47,10 @@ public abstract class NumberFormatterSettings parent; - final int key; - final Object value; - volatile MacroProps resolvedMacros; + private final NumberFormatterSettings parent; + private final int key; + private final Object value; + private volatile MacroProps resolvedMacros; NumberFormatterSettings(NumberFormatterSettings parent, int key, Object value) { this.parent = parent; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java new file mode 100644 index 00000000000..fa0322bffed --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatter.java @@ -0,0 +1,209 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import java.util.Locale; + +import com.ibm.icu.util.ULocale; + +/** + * The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement. + *

+ * Usage example: + *

+ * NumberRangeFormatter.with()
+ *         .identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)
+ *         .numberFormatterFirst(NumberFormatter.with().unit(MeasureUnit.METER))
+ *         .numberFormatterSecond(NumberFormatter.with().unit(MeasureUnit.KILOMETER))
+ *         .locale(ULocale.UK)
+ *         .formatRange(750, 1.2)
+ *         .toString();
+ * // => "750 m - 1.2 km"
+ * 
+ *

+ * Like NumberFormatter, NumberRangeFormatter instances are immutable and thread-safe. This API is based on the + * fluent design pattern popularized by libraries such as Google's Guava. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + */ +public abstract class NumberRangeFormatter { + + /** + * Defines how to merge fields that are identical across the range sign. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public enum RangeCollapse { + /** + * Use locale data and heuristics to determine how much of the string to collapse. Could end up collapsing none, + * some, or all repeated pieces in a locale-sensitive way. + *

+ * The heuristics used for this option are subject to change over time. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + AUTO, + + /** + * Do not collapse any part of the number. Example: "3.2 thousand kilograms – 5.3 thousand kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + NONE, + + /** + * Collapse the unit part of the number, but not the notation, if present. Example: "3.2 thousand – 5.3 thousand + * kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + UNIT, + + /** + * Collapse any field that is equal across the range sign. May introduce ambiguity on the magnitude of the + * number. Example: "3.2 – 5.3 thousand kilograms" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + ALL + } + + /** + * Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect + * when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public static enum RangeIdentityFallback { + /** + * Show the number as a single value rather than a range. Example: "$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding, + * show the single value. Example: "~$5" or "$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + APPROXIMATELY_OR_SINGLE_VALUE, + + /** + * Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the + * inputs are the same. Example: "~$5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + APPROXIMATELY, + + /** + * Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the + * same. Example (with RangeCollapse.NONE): "$5 – $5" + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + RANGE + } + + /** + * Used in the result class FormattedNumberRange to indicate to the user whether the numbers formatted in the range + * were equal or not, and whether or not the identity fallback was applied. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public static enum RangeIdentityResult { + /** + * Used to indicate that the two numbers in the range were equal, even before any rounding rules were applied. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + EQUAL_BEFORE_ROUNDING, + + /** + * Used to indicate that the two numbers in the range were equal, but only after rounding rules were applied. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + EQUAL_AFTER_ROUNDING, + + /** + * Used to indicate that the two numbers in the range were not equal, even after rounding rules were applied. + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + NOT_EQUAL + } + + private static final UnlocalizedNumberRangeFormatter BASE = new UnlocalizedNumberRangeFormatter(); + + /** + * Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is not currently + * known at the call site. + * + * @return An {@link UnlocalizedNumberRangeFormatter}, to be used for chaining. + * @draft ICU 63 + */ + public static UnlocalizedNumberRangeFormatter with() { + return BASE; + } + + /** + * Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call + * site. + * + * @param locale + * The locale from which to load formats and symbols for number range formatting. + * @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining. + * @draft ICU 63 + */ + public static LocalizedNumberRangeFormatter withLocale(Locale locale) { + return BASE.locale(locale); + } + + /** + * Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call + * site. + * + * @param locale + * The locale from which to load formats and symbols for number range formatting. + * @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining. + * @draft ICU 63 + */ + public static LocalizedNumberRangeFormatter withLocale(ULocale locale) { + return BASE.locale(locale); + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java new file mode 100644 index 00000000000..71aafe51cd1 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterImpl.java @@ -0,0 +1,370 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import com.ibm.icu.impl.ICUData; +import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.PatternProps; +import com.ibm.icu.impl.SimpleFormatterImpl; +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.UResource; +import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.MicroProps; +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.SimpleModifier; +import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper; +import com.ibm.icu.impl.number.range.RangeMacroProps; +import com.ibm.icu.impl.number.range.StandardPluralRanges; +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.UResourceBundle; + +/** + * Business logic behind NumberRangeFormatter. + */ +class NumberRangeFormatterImpl { + + final NumberFormatterImpl formatterImpl1; + final NumberFormatterImpl formatterImpl2; + final boolean fSameFormatters; + + final NumberRangeFormatter.RangeCollapse fCollapse; + final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback; + + // Should be final, but they are set in a helper function, not the constructor proper. + // TODO: Clean up to make these fields actually final. + /* final */ String fRangePattern; + /* final */ SimpleModifier fApproximatelyModifier; + + final StandardPluralRanges fPluralRanges; + + //////////////////// + + // Helper function for 2-dimensional switch statement + int identity2d(RangeIdentityFallback a, RangeIdentityResult b) { + return a.ordinal() | (b.ordinal() << 4); + } + + private static final class NumberRangeDataSink extends UResource.Sink { + + String rangePattern; + String approximatelyPattern; + + // For use with SimpleFormatterImpl + StringBuilder sb; + + NumberRangeDataSink(StringBuilder sb) { + this.sb = sb; + } + + @Override + public void put(UResource.Key key, UResource.Value value, boolean noFallback) { + UResource.Table miscTable = value.getTable(); + for (int i = 0; miscTable.getKeyAndValue(i, key, value); ++i) { + if (key.contentEquals("range") && rangePattern == null) { + String pattern = value.getString(); + rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); + } + if (key.contentEquals("approximately") && approximatelyPattern == null) { + String pattern = value.getString(); + approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); + } + } + } + } + + private static void getNumberRangeData( + ULocale locale, + String nsName, + NumberRangeFormatterImpl out) { + StringBuilder sb = new StringBuilder(); + NumberRangeDataSink sink = new NumberRangeDataSink(sb); + ICUResourceBundle resource; + resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); + sb.append("NumberElements/"); + sb.append(nsName); + sb.append("/miscPatterns"); + String key = sb.toString(); + resource.getAllItemsWithFallback(key, sink); + + // TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that? + + if (sink.rangePattern == null) { + sink.rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2); + } + if (sink.approximatelyPattern == null) { + sink.approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1); + } + + out.fRangePattern = sink.rangePattern; + out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false); + } + + //////////////////// + + public NumberRangeFormatterImpl(RangeMacroProps macros) { + formatterImpl1 = new NumberFormatterImpl(macros.formatter1 != null ? macros.formatter1.resolve() + : NumberFormatter.withLocale(macros.loc).resolve()); + formatterImpl2 = new NumberFormatterImpl(macros.formatter2 != null ? macros.formatter2.resolve() + : NumberFormatter.withLocale(macros.loc).resolve()); + fSameFormatters = macros.sameFormatters != 0; + fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO; + fIdentityFallback = macros.identityFallback != null ? macros.identityFallback + : NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY; + + // TODO: As of this writing (ICU 63), there is no locale that has different number miscPatterns + // based on numbering system. Therefore, data is loaded only from latn. If this changes, + // this part of the code should be updated to load from the local numbering system. + // The numbering system could come from the one specified in the NumberFormatter passed to + // numberFormatterBoth() or similar. + // See ICU-20144 + + getNumberRangeData(macros.loc, "latn", this); + + // TODO: Get locale from PluralRules instead? + fPluralRanges = new StandardPluralRanges(macros.loc); + } + + public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) { + NumberStringBuilder string = new NumberStringBuilder(); + MicroProps micros1 = formatterImpl1.preProcess(quantity1); + MicroProps micros2; + if (fSameFormatters) { + micros2 = formatterImpl1.preProcess(quantity2); + } else { + micros2 = formatterImpl2.preProcess(quantity2); + } + + // If any of the affixes are different, an identity is not possible + // and we must use formatRange(). + // TODO: Write this as MicroProps operator==() ? + // TODO: Avoid the redundancy of these equality operations with the + // ones in formatRange? + if (!micros1.modInner.semanticallyEquivalent(micros2.modInner) + || !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle) + || !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) { + formatRange(quantity1, quantity2, string, micros1, micros2); + return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL); + } + + // Check for identity + RangeIdentityResult identityResult; + if (equalBeforeRounding) { + identityResult = RangeIdentityResult.EQUAL_BEFORE_ROUNDING; + } else if (quantity1.equals(quantity2)) { + identityResult = RangeIdentityResult.EQUAL_AFTER_ROUNDING; + } else { + identityResult = RangeIdentityResult.NOT_EQUAL; + } + + // Java does not let us use a constexpr like C++; + // we need to expand identity2d calls. + switch (identity2d(fIdentityFallback, identityResult)) { + case (3 | (2 << 4)): // RANGE, NOT_EQUAL + case (3 | (1 << 4)): // RANGE, EQUAL_AFTER_ROUNDING + case (3 | (0 << 4)): // RANGE, EQUAL_BEFORE_ROUNDING + case (2 | (2 << 4)): // APPROXIMATELY, NOT_EQUAL + case (1 | (2 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, NOT_EQUAL + case (0 | (2 << 4)): // SINGLE_VALUE, NOT_EQUAL + formatRange(quantity1, quantity2, string, micros1, micros2); + break; + + case (2 | (1 << 4)): // APPROXIMATELY, EQUAL_AFTER_ROUNDING + case (2 | (0 << 4)): // APPROXIMATELY, EQUAL_BEFORE_ROUNDING + case (1 | (1 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_AFTER_ROUNDING + formatApproximately(quantity1, quantity2, string, micros1, micros2); + break; + + case (1 | (0 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_BEFORE_ROUNDING + case (0 | (1 << 4)): // SINGLE_VALUE, EQUAL_AFTER_ROUNDING + case (0 | (0 << 4)): // SINGLE_VALUE, EQUAL_BEFORE_ROUNDING + formatSingleValue(quantity1, quantity2, string, micros1, micros2); + break; + + default: + assert false; + break; + } + + return new FormattedNumberRange(string, quantity1, quantity2, identityResult); + } + + private void formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string, + MicroProps micros1, MicroProps micros2) { + if (fSameFormatters) { + int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0); + NumberFormatterImpl.writeAffixes(micros1, string, 0, length); + } else { + formatRange(quantity1, quantity2, string, micros1, micros2); + } + + } + + private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string, + MicroProps micros1, MicroProps micros2) { + if (fSameFormatters) { + int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0); + length += NumberFormatterImpl.writeAffixes(micros1, string, 0, length); + fApproximatelyModifier.apply(string, 0, length); + } else { + formatRange(quantity1, quantity2, string, micros1, micros2); + } + } + + private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string, + MicroProps micros1, MicroProps micros2) { + // modInner is always notation (scientific); collapsable in ALL. + // modOuter is always units; collapsable in ALL, AUTO, and UNIT. + // modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT. + // Never collapse an outer mod but not an inner mod. + boolean collapseOuter, collapseMiddle, collapseInner; + switch (fCollapse) { + case ALL: + case AUTO: + case UNIT: + { + // OUTER MODIFIER + collapseOuter = micros1.modOuter.semanticallyEquivalent(micros2.modOuter); + + if (!collapseOuter) { + // Never collapse inner mods if outer mods are not collapsable + collapseMiddle = false; + collapseInner = false; + break; + } + + // MIDDLE MODIFIER + collapseMiddle = micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle); + + if (!collapseMiddle) { + // Never collapse inner mods if outer mods are not collapsable + collapseInner = false; + break; + } + + // MIDDLE MODIFIER HEURISTICS + // (could disable collapsing of the middle modifier) + // The modifiers are equal by this point, so we can look at just one of them. + Modifier mm = micros1.modMiddle; + if (fCollapse == RangeCollapse.UNIT) { + // Only collapse if the modifier is a unit. + // TODO: Make a better way to check for a unit? + // TODO: Handle case where the modifier has both notation and unit (compact currency)? + if (!mm.containsField(NumberFormat.Field.CURRENCY) && !mm.containsField(NumberFormat.Field.PERCENT)) { + collapseMiddle = false; + } + } else if (fCollapse == RangeCollapse.AUTO) { + // Heuristic as of ICU 63: collapse only if the modifier is more than one code point. + if (mm.getCodePointCount() <= 1) { + collapseMiddle = false; + } + } + + if (!collapseMiddle || fCollapse != RangeCollapse.ALL) { + collapseInner = false; + break; + } + + // INNER MODIFIER + collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner); + + // All done checking for collapsability. + break; + } + + default: + collapseOuter = false; + collapseMiddle = false; + collapseInner = false; + break; + } + + // Java doesn't have macros, constexprs, or stack objects. + // Use a helper object instead. + PrefixInfixSuffixLengthHelper h = new PrefixInfixSuffixLengthHelper(); + + SimpleModifier.formatTwoArgPattern(fRangePattern, string, 0, h, null); + assert h.lengthInfix > 0; + + // SPACING HEURISTIC + // Add spacing unless all modifiers are collapsed. + // TODO: add API to control this? + // TODO: Use a data-driven heuristic like currency spacing? + // TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications) + { + boolean repeatInner = !collapseInner && micros1.modInner.getCodePointCount() > 0; + boolean repeatMiddle = !collapseMiddle && micros1.modMiddle.getCodePointCount() > 0; + boolean repeatOuter = !collapseOuter && micros1.modOuter.getCodePointCount() > 0; + if (repeatInner || repeatMiddle || repeatOuter) { + // Add spacing if there is not already spacing + if (!PatternProps.isWhiteSpace(string.charAt(h.index1()))) { + h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null); + } + if (!PatternProps.isWhiteSpace(string.charAt(h.index2() - 1))) { + h.lengthInfix += string.insertCodePoint(h.index2(), '\u0020', null); + } + } + } + + h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0()); + h.length2 += NumberFormatterImpl.writeNumber(micros2, quantity2, string, h.index2()); + + // TODO: Support padding? + + if (collapseInner) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner); + h.lengthInfix += mod.apply(string, h.index0(), h.index3()); + } else { + h.length1 += micros1.modInner.apply(string, h.index0(), h.index1()); + h.length2 += micros2.modInner.apply(string, h.index2(), h.index3()); + } + + if (collapseMiddle) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle); + h.lengthInfix += mod.apply(string, h.index0(), h.index3()); + } else { + h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1()); + h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3()); + } + + if (collapseOuter) { + // Note: this is actually a mix of prefix and suffix, but adding to infix length works + Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter); + h.lengthInfix += mod.apply(string, h.index0(), h.index3()); + } else { + h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1()); + h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3()); + } + } + + Modifier resolveModifierPlurals(Modifier first, Modifier second) { + Modifier.Parameters firstParameters = first.getParameters(); + if (firstParameters == null) { + // No plural form; return a fallback (e.g., the first) + return first; + } + + Modifier.Parameters secondParameters = second.getParameters(); + if (secondParameters == null) { + // No plural form; return a fallback (e.g., the first) + return first; + } + + // Get the required plural form from data + StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural); + + // Get and return the new Modifier + assert firstParameters.obj == secondParameters.obj; + assert firstParameters.signum == secondParameters.signum; + Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural); + assert mod != null; + return mod; + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java new file mode 100644 index 00000000000..50df9af21bc --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberRangeFormatterSettings.java @@ -0,0 +1,240 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import com.ibm.icu.impl.number.range.RangeMacroProps; +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.util.ULocale; + +/** + * An abstract base class for specifying settings related to number formatting. This class is implemented by + * {@link UnlocalizedNumberRangeFormatter} and {@link LocalizedNumberRangeFormatter}. This class is not intended for + * public subclassing. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public abstract class NumberRangeFormatterSettings> { + + static final int KEY_MACROS = 0; // not used + static final int KEY_LOCALE = 1; + static final int KEY_FORMATTER_1 = 2; + static final int KEY_FORMATTER_2 = 3; + static final int KEY_SAME_FORMATTERS = 4; + static final int KEY_COLLAPSE = 5; + static final int KEY_IDENTITY_FALLBACK = 6; + static final int KEY_MAX = 7; + + private final NumberRangeFormatterSettings parent; + private final int key; + private final Object value; + private volatile RangeMacroProps resolvedMacros; + + NumberRangeFormatterSettings(NumberRangeFormatterSettings parent, int key, Object value) { + this.parent = parent; + this.key = key; + this.value = value; + } + + /** + * Sets the NumberFormatter instance to use for the numbers in the range. The same formatter is applied to both + * sides of the range. + *

+ * The NumberFormatter instances must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatter + * The formatter to use for both numbers in the range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + * @see NumberRangeFormatter + */ + @SuppressWarnings("unchecked") + public T numberFormatterBoth(UnlocalizedNumberFormatter formatter) { + return (T) create(KEY_SAME_FORMATTERS, true).create(KEY_FORMATTER_1, formatter); + } + + /** + * Sets the NumberFormatter instance to use for the first number in the range. + *

+ * The NumberFormatter instance must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatterFirst + * The formatter to use for the first number in the range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + * @see NumberRangeFormatter + */ + @SuppressWarnings("unchecked") + public T numberFormatterFirst(UnlocalizedNumberFormatter formatterFirst) { + return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_1, formatterFirst); + } + + /** + * Sets the NumberFormatter instances to use for the second number in the range. + *

+ * The NumberFormatter instance must not have a locale applied yet; the locale specified on the + * NumberRangeFormatter will be used. + * + * @param formatterSecond + * The formatter to use for the second number in the range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberFormatter + * @see NumberRangeFormatter + */ + @SuppressWarnings("unchecked") + public T numberFormatterSecond(UnlocalizedNumberFormatter formatterSecond) { + return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_2, formatterSecond); + } + + /** + * Sets the aggressiveness of "collapsing" fields across the range separator. Possible values: + *

    + *
  • ALL: "3-5K miles"
  • + *
  • UNIT: "3K - 5K miles"
  • + *
  • NONE: "3K miles - 5K miles"
  • + *
  • AUTO: usually UNIT or NONE, depending on the locale and formatter settings
  • + *
+ *

+ * The default value is AUTO. + * + * @param collapse + * The collapsing strategy to use for this range. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public T collapse(RangeCollapse collapse) { + return create(KEY_COLLAPSE, collapse); + } + + /** + * Sets the behavior when the two sides of the range are the same. This could happen if the same two numbers are + * passed to the formatRange function, or if different numbers are passed to the function but they become the same + * after rounding rules are applied. Possible values: + *

    + *
  • SINGLE_VALUE: "5 miles"
  • + *
  • APPROXIMATELY_OR_SINGLE_VALUE: "~5 miles" or "5 miles", depending on whether the number was the same before + * rounding was applied
  • + *
  • APPROXIMATELY: "~5 miles"
  • + *
  • RANGE: "5-5 miles" (with collapse=UNIT)
  • + *
+ *

+ * The default value is APPROXIMATELY. + * + * @param identityFallback + * The strategy to use when formatting two numbers that end up being the same. + * @return The fluent chain. + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ + public T identityFallback(RangeIdentityFallback identityFallback) { + return create(KEY_IDENTITY_FALLBACK, identityFallback); + } + + /* package-protected */ abstract T create(int key, Object value); + + RangeMacroProps resolve() { + if (resolvedMacros != null) { + return resolvedMacros; + } + // Although the linked-list fluent storage approach requires this method, + // my benchmarks show that linked-list is still faster than a full clone + // of a MacroProps object at each step. + // TODO: Remove the reference to the parent after the macros are resolved? + RangeMacroProps macros = new RangeMacroProps(); + NumberRangeFormatterSettings current = this; + while (current != null) { + switch (current.key) { + case KEY_MACROS: + // ignored for now + break; + case KEY_LOCALE: + if (macros.loc == null) { + macros.loc = (ULocale) current.value; + } + break; + case KEY_FORMATTER_1: + if (macros.formatter1 == null) { + macros.formatter1 = (UnlocalizedNumberFormatter) current.value; + } + break; + case KEY_FORMATTER_2: + if (macros.formatter2 == null) { + macros.formatter2 = (UnlocalizedNumberFormatter) current.value; + } + break; + case KEY_SAME_FORMATTERS: + if (macros.sameFormatters == -1) { + macros.sameFormatters = (boolean) current.value ? 1 : 0; + } + break; + case KEY_COLLAPSE: + if (macros.collapse == null) { + macros.collapse = (RangeCollapse) current.value; + } + break; + case KEY_IDENTITY_FALLBACK: + if (macros.identityFallback == null) { + macros.identityFallback = (RangeIdentityFallback) current.value; + } + break; + default: + throw new AssertionError("Unknown key: " + current.key); + } + current = current.parent; + } + // Copy the locale into the children (see touchRangeLocales in C++) + if (macros.formatter1 != null) { + macros.formatter1.resolve().loc = macros.loc; + } + if (macros.formatter2 != null) { + macros.formatter2.resolve().loc = macros.loc; + } + resolvedMacros = macros; + return macros; + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int hashCode() { + return resolve().hashCode(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof NumberRangeFormatterSettings)) { + return false; + } + return resolve().equals(((NumberRangeFormatterSettings) other).resolve()); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java b/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java index d3f62b963e3..bd0c723b859 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java @@ -13,6 +13,7 @@ import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.Precision.SignificantRounderImpl; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.NumberFormat.Field; /** * A class that defines the scientific notation style to be used when formatting numbers in @@ -221,8 +222,10 @@ public class ScientificNotation extends Notation implements Cloneable { @Override public int getCodePointCount() { - // This method is not used for strong modifiers. - throw new AssertionError(); + // NOTE: This method is only called one place, NumberRangeFormatterImpl. + // The call site only cares about != 0 and != 1. + // Return a very large value so that if this method is used elsewhere, we should notice. + return 999; } @Override @@ -231,6 +234,27 @@ public class ScientificNotation extends Notation implements Cloneable { return true; } + @Override + public boolean containsField(Field field) { + // This method is not currently used. (unsafe path not used in range formatting) + assert false; + return false; + } + + @Override + public Parameters getParameters() { + // This method is not currently used. + assert false; + return null; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + // This method is not currently used. (unsafe path not used in range formatting) + assert false; + return false; + } + @Override public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { return doApply(exponent, output, rightIndex); @@ -279,8 +303,10 @@ public class ScientificNotation extends Notation implements Cloneable { @Override public int getCodePointCount() { - // This method is not used for strong modifiers. - throw new AssertionError(); + // NOTE: This method is only called one place, NumberRangeFormatterImpl. + // The call site only cares about != 0 and != 1. + // Return a very large value so that if this method is used elsewhere, we should notice. + return 999; } @Override @@ -288,5 +314,27 @@ public class ScientificNotation extends Notation implements Cloneable { // Scientific is always strong return true; } + + @Override + public boolean containsField(Field field) { + // This method is not used for inner modifiers. + assert false; + return false; + } + + @Override + public Parameters getParameters() { + return null; + } + + @Override + public boolean semanticallyEquivalent(Modifier other) { + if (!(other instanceof ScientificModifier)) { + return false; + } + ScientificModifier _other = (ScientificModifier) other; + // TODO: Check for locale symbols and settings as well? Could be less efficient. + return exponent == _other.exponent; + } } } \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java new file mode 100644 index 00000000000..c3ee21e43d0 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/UnlocalizedNumberRangeFormatter.java @@ -0,0 +1,67 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.number; + +import java.util.Locale; + +import com.ibm.icu.util.ULocale; + +/** + * A NumberRangeFormatter that does not yet have a locale. In order to format, a locale must be specified. + * + * @author sffc + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + * @see NumberRangeFormatter + */ +public class UnlocalizedNumberRangeFormatter extends NumberRangeFormatterSettings { + + /** Base constructor; called during startup only. */ + UnlocalizedNumberRangeFormatter() { + super(null, KEY_MACROS, null); + } + + UnlocalizedNumberRangeFormatter(NumberRangeFormatterSettings parent, int key, Object value) { + super(parent, key, value); + } + + /** + * Associate the given locale with the number range formatter. The locale is used for picking the + * appropriate symbols, formats, and other data for number display. + * + *

+ * To use the Java default locale, call Locale.getDefault(): + * + *

+     * NumberFormatter.with(). ... .locale(Locale.getDefault())
+     * 
+ * + * @param locale + * The locale to use when loading data for number range formatting. + * @return The fluent chain + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + public LocalizedNumberRangeFormatter locale(Locale locale) { + return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, ULocale.forLocale(locale)); + } + + /** + * ULocale version of the {@link #locale(Locale)} setter above. + * + * @param locale + * The locale to use when loading data for number range formatting. + * @return The fluent chain + * @see #locale(Locale) + * @draft ICU 63 + * @provisional This API might change or be removed in a future release. + */ + public LocalizedNumberRangeFormatter locale(ULocale locale) { + return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, locale); + } + + @Override + UnlocalizedNumberRangeFormatter create(int key, Object value) { + return new UnlocalizedNumberRangeFormatter(this, key, value); + } +} diff --git a/icu4j/main/shared/data/icudata.jar b/icu4j/main/shared/data/icudata.jar index e2be2f06c93..19219d0d605 100644 --- a/icu4j/main/shared/data/icudata.jar +++ b/icu4j/main/shared/data/icudata.jar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92dc0a5ca71ac54537a6c7c42c2f80ccbd3298d9ebf69c3d732199230d5100c6 -size 12487439 +oid sha256:6dadae4ae83d956325f4e089327e824bb32dbf75c98e9c2815ffa7521f3505ca +size 12512661 diff --git a/icu4j/main/shared/data/icutzdata.jar b/icu4j/main/shared/data/icutzdata.jar index 6d6bf299cc0..d4ac91fdfbb 100755 --- a/icu4j/main/shared/data/icutzdata.jar +++ b/icu4j/main/shared/data/icutzdata.jar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2308b3498ce1c2b869b60b5a0f7cea6f08aff2fac046ae260e10688c15c60b2 +oid sha256:b5ffb95eb91501a9f61ee31333e392ff183e0f7dddefb592f04b90fe2ac3490c size 92857 diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java new file mode 100644 index 00000000000..52933d0b271 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberRangeFormatterTest.java @@ -0,0 +1,726 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +import static org.junit.Assert.assertEquals; + +import java.util.Locale; + +import org.junit.Test; + +import com.ibm.icu.number.LocalizedNumberFormatter; +import com.ibm.icu.number.LocalizedNumberRangeFormatter; +import com.ibm.icu.number.Notation; +import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.NumberFormatter.UnitWidth; +import com.ibm.icu.number.NumberRangeFormatter; +import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse; +import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback; +import com.ibm.icu.number.Precision; +import com.ibm.icu.number.UnlocalizedNumberFormatter; +import com.ibm.icu.number.UnlocalizedNumberRangeFormatter; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +/** + * @author sffc + * + */ +public class NumberRangeFormatterTest { + + private static final Currency USD = Currency.getInstance("USD"); + private static final Currency GBP = Currency.getInstance("GBP"); + private static final Currency PTE = Currency.getInstance("PTE"); + + @Test + public void testSanity() { + LocalizedNumberRangeFormatter lnrf1 = NumberRangeFormatter.withLocale(ULocale.US); + LocalizedNumberRangeFormatter lnrf2 = NumberRangeFormatter.with().locale(ULocale.US); + LocalizedNumberRangeFormatter lnrf3 = NumberRangeFormatter.withLocale(Locale.US); + LocalizedNumberRangeFormatter lnrf4 = NumberRangeFormatter.with().locale(Locale.US); + assertEquals("Formatters should be equal 1", lnrf1, lnrf2); + assertEquals("Formatters should be equal 2", lnrf2, lnrf3); + assertEquals("Formatters should be equal 3", lnrf3, lnrf4); + assertEquals("Formatters should have same behavior 1", lnrf1.formatRange(4, 6), lnrf2.formatRange(4, 6)); + assertEquals("Formatters should have same behavior 2", lnrf2.formatRange(4, 6), lnrf3.formatRange(4, 6)); + assertEquals("Formatters should have same behavior 3", lnrf3.formatRange(4, 6), lnrf4.formatRange(4, 6)); + } + + @Test + public void testBasic() { + assertFormatRange( + "Basic", + NumberRangeFormatter.with(), + new ULocale("en-us"), + "1–5", + "~5", + "~5", + "0–3", + "~0", + "3–3,000", + "3,000–5,000", + "4,999–5,001", + "~5,000", + "5,000–5,000,000"); + + assertFormatRange( + "Basic with units", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3,000 m", + "3,000–5,000 m", + "4,999–5,001 m", + "~5,000 m", + "5,000–5,000,000 m"); + + assertFormatRange( + "Basic with different units", + NumberRangeFormatter.with() + .numberFormatterFirst(NumberFormatter.with().unit(MeasureUnit.METER)) + .numberFormatterSecond(NumberFormatter.with().unit(MeasureUnit.KILOMETER)), + new ULocale("en-us"), + "1 m – 5 km", + "5 m – 5 km", + "5 m – 5 km", + "0 m – 3 km", + "0 m – 0 km", + "3 m – 3,000 km", + "3,000 m – 5,000 km", + "4,999 m – 5,001 km", + "5,000 m – 5,000 km", + "5,000 m – 5,000,000 km"); + + assertFormatRange( + "Basic long unit", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME)), + new ULocale("en-us"), + "1–5 meters", + "~5 meters", + "~5 meters", + "0–3 meters", + "~0 meters", + "3–3,000 meters", + "3,000–5,000 meters", + "4,999–5,001 meters", + "~5,000 meters", + "5,000–5,000,000 meters"); + + assertFormatRange( + "Non-English locale and unit", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME)), + new ULocale("fr-FR"), + "1–5 degrés Fahrenheit", + "~5 degrés Fahrenheit", + "~5 degrés Fahrenheit", + "0–3 degrés Fahrenheit", + "~0 degré Fahrenheit", + "3–3 000 degrés Fahrenheit", + "3 000–5 000 degrés Fahrenheit", + "4 999–5 001 degrés Fahrenheit", + "~5 000 degrés Fahrenheit", + "5 000–5 000 000 degrés Fahrenheit"); + + assertFormatRange( + "Locale with custom range separator", + NumberRangeFormatter.with(), + new ULocale("ja"), + "1~5", + "~5", + "~5", + "0~3", + "~0", + "3~3,000", + "3,000~5,000", + "4,999~5,001", + "~5,000", + "5,000~5,000,000"); + + assertFormatRange( + "Locale that already has spaces around range separator", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.KELVIN)), + new ULocale("hr"), + "1 K – 5 K", + "~5 K", + "~5 K", + "0 K – 3 K", + "~0 K", + "3 K – 3.000 K", + "3.000 K – 5.000 K", + "4.999 K – 5.001 K", + "~5.000 K", + "5.000 K – 5.000.000 K"); + + assertFormatRange( + "Locale with custom numbering system and no plural ranges data", + NumberRangeFormatter.with(), + new ULocale("shn@numbers=beng"), + // 012459 = ০১৩৪৫৯ + "১–৫", + "~৫", + "~৫", + "০–৩", + "~০", + "৩–৩,০০০", + "৩,০০০–৫,০০০", + "৪,৯৯৯–৫,০০১", + "~৫,০০০", + "৫,০০০–৫,০০০,০০০"); + + assertFormatRange( + "Portuguese currency", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(PTE)), + new ULocale("pt-PT"), + "1$00 - 5$00 \u200B", + "~5$00 \u200B", + "~5$00 \u200B", + "0$00 - 3$00 \u200B", + "~0$00 \u200B", + "3$00 - 3000$00 \u200B", + "3000$00 - 5000$00 \u200B", + "4999$00 - 5001$00 \u200B", + "~5000$00 \u200B", + "5000$00 - 5,000,000$00 \u200B"); + } + + @Test + public void testCollapse() { + assertFormatRange( + "Default collapse on currency (default rounding)", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(USD)), + new ULocale("en-us"), + "$1.00 – $5.00", + "~$5.00", + "~$5.00", + "$0.00 – $3.00", + "~$0.00", + "$3.00 – $3,000.00", + "$3,000.00 – $5,000.00", + "$4,999.00 – $5,001.00", + "~$5,000.00", + "$5,000.00 – $5,000,000.00"); + + assertFormatRange( + "Default collapse on currency", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())), + new ULocale("en-us"), + "$1 – $5", + "~$5", + "~$5", + "$0 – $3", + "~$0", + "$3 – $3,000", + "$3,000 – $5,000", + "$4,999 – $5,001", + "~$5,000", + "$5,000 – $5,000,000"); + + assertFormatRange( + "No collapse on currency", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())), + new ULocale("en-us"), + "$1 – $5", + "~$5", + "~$5", + "$0 – $3", + "~$0", + "$3 – $3,000", + "$3,000 – $5,000", + "$4,999 – $5,001", + "~$5,000", + "$5,000 – $5,000,000"); + + assertFormatRange( + "Unit collapse on currency", + NumberRangeFormatter.with() + .collapse(RangeCollapse.UNIT) + .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())), + new ULocale("en-us"), + "$1–5", + "~$5", + "~$5", + "$0–3", + "~$0", + "$3–3,000", + "$3,000–5,000", + "$4,999–5,001", + "~$5,000", + "$5,000–5,000,000"); + + assertFormatRange( + "All collapse on currency", + NumberRangeFormatter.with() + .collapse(RangeCollapse.ALL) + .numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())), + new ULocale("en-us"), + "$1–5", + "~$5", + "~$5", + "$0–3", + "~$0", + "$3–3,000", + "$3,000–5,000", + "$4,999–5,001", + "~$5,000", + "$5,000–5,000,000"); + + assertFormatRange( + "Default collapse on currency ISO code", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with() + .unit(GBP) + .unitWidth(UnitWidth.ISO_CODE) + .precision(Precision.integer())), + new ULocale("en-us"), + "GBP 1–5", + "~GBP 5", // TODO: Fix this at some point + "~GBP 5", + "GBP 0–3", + "~GBP 0", + "GBP 3–3,000", + "GBP 3,000–5,000", + "GBP 4,999–5,001", + "~GBP 5,000", + "GBP 5,000–5,000,000"); + + assertFormatRange( + "No collapse on currency ISO code", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with() + .unit(GBP) + .unitWidth(UnitWidth.ISO_CODE) + .precision(Precision.integer())), + new ULocale("en-us"), + "GBP 1 – GBP 5", + "~GBP 5", // TODO: Fix this at some point + "~GBP 5", + "GBP 0 – GBP 3", + "~GBP 0", + "GBP 3 – GBP 3,000", + "GBP 3,000 – GBP 5,000", + "GBP 4,999 – GBP 5,001", + "~GBP 5,000", + "GBP 5,000 – GBP 5,000,000"); + + assertFormatRange( + "Unit collapse on currency ISO code", + NumberRangeFormatter.with() + .collapse(RangeCollapse.UNIT) + .numberFormatterBoth(NumberFormatter.with() + .unit(GBP) + .unitWidth(UnitWidth.ISO_CODE) + .precision(Precision.integer())), + new ULocale("en-us"), + "GBP 1–5", + "~GBP 5", // TODO: Fix this at some point + "~GBP 5", + "GBP 0–3", + "~GBP 0", + "GBP 3–3,000", + "GBP 3,000–5,000", + "GBP 4,999–5,001", + "~GBP 5,000", + "GBP 5,000–5,000,000"); + + assertFormatRange( + "All collapse on currency ISO code", + NumberRangeFormatter.with() + .collapse(RangeCollapse.ALL) + .numberFormatterBoth(NumberFormatter.with() + .unit(GBP) + .unitWidth(UnitWidth.ISO_CODE) + .precision(Precision.integer())), + new ULocale("en-us"), + "GBP 1–5", + "~GBP 5", // TODO: Fix this at some point + "~GBP 5", + "GBP 0–3", + "~GBP 0", + "GBP 3–3,000", + "GBP 3,000–5,000", + "GBP 4,999–5,001", + "~GBP 5,000", + "GBP 5,000–5,000,000"); + + // Default collapse on measurement unit is in testBasic() + + assertFormatRange( + "No collapse on measurement unit", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1 m – 5 m", + "~5 m", + "~5 m", + "0 m – 3 m", + "~0 m", + "3 m – 3,000 m", + "3,000 m – 5,000 m", + "4,999 m – 5,001 m", + "~5,000 m", + "5,000 m – 5,000,000 m"); + + assertFormatRange( + "Unit collapse on measurement unit", + NumberRangeFormatter.with() + .collapse(RangeCollapse.UNIT) + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3,000 m", + "3,000–5,000 m", + "4,999–5,001 m", + "~5,000 m", + "5,000–5,000,000 m"); + + assertFormatRange( + "All collapse on measurement unit", + NumberRangeFormatter.with() + .collapse(RangeCollapse.ALL) + .numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3,000 m", + "3,000–5,000 m", + "4,999–5,001 m", + "~5,000 m", + "5,000–5,000,000 m"); + + assertFormatRange( + "Default collapse, long-form compact notation", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())), + new ULocale("de-CH"), + "1–5", + "~5", + "~5", + "0–3", + "~0", + "3–3 Tausend", + "3–5 Tausend", + "~5 Tausend", + "~5 Tausend", + "5 Tausend – 5 Millionen"); + + assertFormatRange( + "Unit collapse, long-form compact notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.UNIT) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())), + new ULocale("de-CH"), + "1–5", + "~5", + "~5", + "0–3", + "~0", + "3–3 Tausend", + "3 Tausend – 5 Tausend", + "~5 Tausend", + "~5 Tausend", + "5 Tausend – 5 Millionen"); + + assertFormatRange( + "Default collapse on measurement unit with compact-short notation", + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3K m", + "3K – 5K m", + "~5K m", + "~5K m", + "5K – 5M m"); + + assertFormatRange( + "No collapse on measurement unit with compact-short notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1 m – 5 m", + "~5 m", + "~5 m", + "0 m – 3 m", + "~0 m", + "3 m – 3K m", + "3K m – 5K m", + "~5K m", + "~5K m", + "5K m – 5M m"); + + assertFormatRange( + "Unit collapse on measurement unit with compact-short notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.UNIT) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3K m", + "3K – 5K m", + "~5K m", + "~5K m", + "5K – 5M m"); + + assertFormatRange( + "All collapse on measurement unit with compact-short notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.ALL) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)), + new ULocale("en-us"), + "1–5 m", + "~5 m", + "~5 m", + "0–3 m", + "~0 m", + "3–3K m", + "3–5K m", // this one is the key use case for ALL + "~5K m", + "~5K m", + "5K – 5M m"); + + assertFormatRange( + "No collapse on scientific notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.NONE) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())), + new ULocale("en-us"), + "1E0 – 5E0", + "~5E0", + "~5E0", + "0E0 – 3E0", + "~0E0", + "3E0 – 3E3", + "3E3 – 5E3", + "4.999E3 – 5.001E3", + "~5E3", + "5E3 – 5E6"); + + assertFormatRange( + "All collapse on scientific notation", + NumberRangeFormatter.with() + .collapse(RangeCollapse.ALL) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())), + new ULocale("en-us"), + "1–5E0", + "~5E0", + "~5E0", + "0–3E0", + "~0E0", + "3E0 – 3E3", + "3–5E3", + "4.999–5.001E3", + "~5E3", + "5E3 – 5E6"); + + // TODO: Test compact currency? + // The code is not smart enough to differentiate the notation from the unit. + } + + @Test + public void testIdentity() { + assertFormatRange( + "Identity fallback Range", + NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.RANGE), + new ULocale("en-us"), + "1–5", + "5–5", + "5–5", + "0–3", + "0–0", + "3–3,000", + "3,000–5,000", + "4,999–5,001", + "5,000–5,000", + "5,000–5,000,000"); + + assertFormatRange( + "Identity fallback Approximately or Single Value", + NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE), + new ULocale("en-us"), + "1–5", + "~5", + "5", + "0–3", + "0", + "3–3,000", + "3,000–5,000", + "4,999–5,001", + "5,000", + "5,000–5,000,000"); + + assertFormatRange( + "Identity fallback Single Value", + NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.SINGLE_VALUE), + new ULocale("en-us"), + "1–5", + "5", + "5", + "0–3", + "0", + "3–3,000", + "3,000–5,000", + "4,999–5,001", + "5,000", + "5,000–5,000,000"); + + assertFormatRange( + "Identity fallback Approximately or Single Value with compact notation", + NumberRangeFormatter.with() + .identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE) + .numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort())), + new ULocale("en-us"), + "1–5", + "~5", + "5", + "0–3", + "0", + "3–3K", + "3K – 5K", + "~5K", + "5K", + "5K – 5M"); + } + + @Test + public void testDifferentFormatters() { + assertFormatRange( + "Different rounding rules", + NumberRangeFormatter.with() + .numberFormatterFirst(NumberFormatter.with().precision(Precision.integer())) + .numberFormatterSecond(NumberFormatter.with().precision(Precision.fixedDigits(2))), + new ULocale("en-us"), + "1–5.0", + "5–5.0", + "5–5.0", + "0–3.0", + "0–0.0", + "3–3,000", + "3,000–5,000", + "4,999–5,000", + "5,000–5,000", // TODO: Should this one be ~5,000? + "5,000–5,000,000"); + } + + @Test + public void testPlurals() { + // Locale sl has interesting plural forms: + // GBP{ + // one{"britanski funt"} + // two{"britanska funta"} + // few{"britanski funti"} + // other{"britanskih funtov"} + // } + ULocale locale = new ULocale("sl"); + + UnlocalizedNumberFormatter unf = NumberFormatter.with() + .unit(GBP) + .unitWidth(UnitWidth.FULL_NAME) + .precision(Precision.integer()); + LocalizedNumberFormatter lnf = unf.locale(locale); + + // For comparison, run the non-range version of the formatter + assertEquals(Integer.toString(1), "1 britanski funt", lnf.format(1).toString()); + assertEquals(Integer.toString(2), "2 britanska funta", lnf.format(2).toString()); + assertEquals(Integer.toString(3), "3 britanski funti", lnf.format(3).toString()); + assertEquals(Integer.toString(5), "5 britanskih funtov", lnf.format(5).toString()); + + LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter.with() + .numberFormatterBoth(unf) + .identityFallback(RangeIdentityFallback.RANGE) + .locale(locale); + + Object[][] cases = new Object[][] { + {1, 1, "1–1 britanski funti"}, // one + one -> few + {1, 2, "1–2 britanska funta"}, // one + two -> two + {1, 3, "1–3 britanski funti"}, // one + few -> few + {1, 5, "1–5 britanskih funtov"}, // one + other -> other + {2, 1, "2–1 britanski funti"}, // two + one -> few + {2, 2, "2–2 britanska funta"}, // two + two -> two + {2, 3, "2–3 britanski funti"}, // two + few -> few + {2, 5, "2–5 britanskih funtov"}, // two + other -> other + {3, 1, "3–1 britanski funti"}, // few + one -> few + {3, 2, "3–2 britanska funta"}, // few + two -> two + {3, 3, "3–3 britanski funti"}, // few + few -> few + {3, 5, "3–5 britanskih funtov"}, // few + other -> other + {5, 1, "5–1 britanski funti"}, // other + one -> few + {5, 2, "5–2 britanska funta"}, // other + two -> two + {5, 3, "5–3 britanski funti"}, // other + few -> few + {5, 5, "5–5 britanskih funtov"}, // other + other -> other + }; + for (Object[] cas : cases) { + int first = (Integer) cas[0]; + int second = (Integer) cas[1]; + String expected = (String) cas[2]; + String message = Integer.toString(first) + " " + Integer.toString(second); + String actual = lnrf.formatRange(first, second).toString(); + assertEquals(message, expected, actual); + } + } + + static void assertFormatRange( + String message, + UnlocalizedNumberRangeFormatter f, + ULocale locale, + String expected_10_50, + String expected_49_51, + String expected_50_50, + String expected_00_30, + String expected_00_00, + String expected_30_3K, + String expected_30K_50K, + String expected_49K_51K, + String expected_50K_50K, + String expected_50K_50M) { + LocalizedNumberRangeFormatter l = f.locale(locale); + assertFormattedRangeEquals(message, l, 1, 5, expected_10_50); + assertFormattedRangeEquals(message, l, 4.9999999, 5.0000001, expected_49_51); + assertFormattedRangeEquals(message, l, 5, 5, expected_50_50); + assertFormattedRangeEquals(message, l, 0, 3, expected_00_30); + assertFormattedRangeEquals(message, l, 0, 0, expected_00_00); + assertFormattedRangeEquals(message, l, 3, 3000, expected_30_3K); + assertFormattedRangeEquals(message, l, 3000, 5000, expected_30K_50K); + assertFormattedRangeEquals(message, l, 4999, 5001, expected_49K_51K); + assertFormattedRangeEquals(message, l, 5000, 5000, expected_50K_50K); + assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M); + } + + private static void assertFormattedRangeEquals(String message, LocalizedNumberRangeFormatter l, Number first, + Number second, String expected) { + String actual = l.formatRange(first, second).toString(); + assertEquals(message + ": " + first + ", " + second, expected, actual); + } + +}