diff --git a/icu4c/source/common/capi_helper.h b/icu4c/source/common/capi_helper.h index 890488211e3..54b1db9e331 100644 --- a/icu4c/source/common/capi_helper.h +++ b/icu4c/source/common/capi_helper.h @@ -25,7 +25,12 @@ class IcuCApiHelper { static CPPType* validate(CType* input, UErrorCode& status); /** - * Convert from the C++ type to the C type. + * Convert from the C++ type to the C type (const version). + */ + const CType* exportConstForC() const; + + /** + * Convert from the C++ type to the C type (non-const version). */ CType* exportForC(); @@ -53,7 +58,7 @@ IcuCApiHelper::validate(const CType* input, UErrorCode& return nullptr; } auto* impl = reinterpret_cast(input); - if (impl->fMagic != kMagic) { + if (static_cast*>(impl)->fMagic != kMagic) { status = U_INVALID_FORMAT_ERROR; return nullptr; } @@ -68,6 +73,12 @@ IcuCApiHelper::validate(CType* input, UErrorCode& status return const_cast(validated); } +template +const CType* +IcuCApiHelper::exportConstForC() const { + return reinterpret_cast(static_cast(this)); +} + template CType* IcuCApiHelper::exportForC() { diff --git a/icu4c/source/i18n/Makefile.in b/icu4c/source/i18n/Makefile.in index fb5eb146bec..65705929b44 100644 --- a/icu4c/source/i18n/Makefile.in +++ b/icu4c/source/i18n/Makefile.in @@ -112,7 +112,7 @@ 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 +erarules.o formattedvalue.o ## Header files to install HEADERS = $(srcdir)/unicode/*.h diff --git a/icu4c/source/i18n/formattedvalue.cpp b/icu4c/source/i18n/formattedvalue.cpp new file mode 100644 index 00000000000..fa1888994f2 --- /dev/null +++ b/icu4c/source/i18n/formattedvalue.cpp @@ -0,0 +1,205 @@ +// © 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 "unicode/formattedvalue.h" +#include "number_utypes.h" +#include "capi_helper.h" + +U_NAMESPACE_BEGIN + + +ConstrainedFieldPosition::ConstrainedFieldPosition() {} + +ConstrainedFieldPosition::~ConstrainedFieldPosition() {} + +void ConstrainedFieldPosition::reset() { + fContext = 0LL; + fField = 0; + fStart = 0; + fLimit = 0; + fConstraint = UCFPOS_CONSTRAINT_NONE; + fCategory = UFIELD_CATEGORY_UNDEFINED; +} + +void ConstrainedFieldPosition::constrainCategory(UFieldCategory category) { + fConstraint = UCFPOS_CONSTRAINT_CATEGORY; + fCategory = category; +} + +void ConstrainedFieldPosition::constrainField(UFieldCategory category, int32_t field) { + fConstraint = UCFPOS_CONSTRAINT_FIELD; + fCategory = category; + fField = field; +} + +void ConstrainedFieldPosition::setInt64IterationContext(int64_t context) { + fContext = context; +} + +void ConstrainedFieldPosition::setState( + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit) { + fCategory = category; + fField = field; + fStart = start; + fLimit = limit; +} + + +FormattedValue::~FormattedValue() = default; + + +/////////////////////// +/// C API FUNCTIONS /// +/////////////////////// + +struct UConstrainedFieldPositionImpl : public UMemory, + // Magic number as ASCII == "UCF" + public IcuCApiHelper { + ConstrainedFieldPosition fImpl; +}; + +U_CAPI UConstrainedFieldPosition* U_EXPORT2 +ucfpos_open(UErrorCode* ec) { + auto* impl = new UConstrainedFieldPositionImpl(); + if (impl == nullptr) { + *ec = U_MEMORY_ALLOCATION_ERROR; + return nullptr; + } + return impl->exportForC(); +} + +U_CAPI void U_EXPORT2 +ucfpos_reset(UConstrainedFieldPosition* ptr, UErrorCode* ec) { + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + impl->fImpl.reset(); +} + +U_CAPI void U_EXPORT2 +ucfpos_constrainCategory(UConstrainedFieldPosition* ptr, UFieldCategory category, UErrorCode* ec) { + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + impl->fImpl.constrainCategory(category); +} + +U_CAPI void U_EXPORT2 +ucfpos_constrainField(UConstrainedFieldPosition* ptr, UFieldCategory category, int32_t field, UErrorCode* ec) { + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + impl->fImpl.constrainField(category, field); +} + +U_CAPI UCFPosConstraintType U_EXPORT2 +ucfpos_getConstraintType(const UConstrainedFieldPosition* ptr, UErrorCode* ec) { + const auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return UCFPOS_CONSTRAINT_NONE; + } + return impl->fImpl.getConstraintType(); +} + +U_CAPI UFieldCategory U_EXPORT2 +ucfpos_getCategory(const UConstrainedFieldPosition* ptr, UErrorCode* ec) { + const auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return UFIELD_CATEGORY_UNDEFINED; + } + return impl->fImpl.getCategory(); +} + +U_CAPI int32_t U_EXPORT2 +ucfpos_getField(const UConstrainedFieldPosition* ptr, UErrorCode* ec) { + const auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return 0; + } + return impl->fImpl.getField(); +} + +U_CAPI void U_EXPORT2 +ucfpos_getIndexes(const UConstrainedFieldPosition* ptr, int32_t* pStart, int32_t* pLimit, UErrorCode* ec) { + const auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + *pStart = impl->fImpl.getStart(); + *pLimit = impl->fImpl.getLimit(); +} + +U_CAPI int64_t U_EXPORT2 +ucfpos_getInt64IterationContext(const UConstrainedFieldPosition* ptr, UErrorCode* ec) { + const auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return 0; + } + return impl->fImpl.getInt64IterationContext(); +} + +U_CAPI void U_EXPORT2 +ucfpos_setInt64IterationContext(UConstrainedFieldPosition* ptr, int64_t context, UErrorCode* ec) { + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + impl->fImpl.setInt64IterationContext(context); +} + +U_CAPI void U_EXPORT2 +ucfpos_setState( + UConstrainedFieldPosition* ptr, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + UErrorCode* ec) { + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, *ec); + if (U_FAILURE(*ec)) { + return; + } + impl->fImpl.setState(category, field, start, limit); +} + +U_CAPI void U_EXPORT2 +ucfpos_close(UConstrainedFieldPosition* ptr) { + UErrorCode localStatus = U_ZERO_ERROR; + auto* impl = UConstrainedFieldPositionImpl::validate(ptr, localStatus); + delete impl; +} + + +U_DRAFT const UChar* U_EXPORT2 +ufmtval_getString( + const UFormattedValue* ufmtval, + int32_t* pLength, + UErrorCode* ec) { + const auto* impl = number::impl::UFormattedValueApiHelper::validate(ufmtval, *ec); + if (U_FAILURE(*ec)) { + return nullptr; + } + UnicodeString readOnlyAlias = impl->fFormattedValue->toTempString(*ec); + if (U_FAILURE(*ec)) { + return nullptr; + } + if (pLength != nullptr) { + *pLength = readOnlyAlias.length(); + } + return readOnlyAlias.getBuffer(); +} + + +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index 32396817aa6..57cdb8da7be 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -238,6 +238,7 @@ + diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters index c8f98451427..dc90bfce517 100644 --- a/icu4c/source/i18n/i18n.vcxproj.filters +++ b/icu4c/source/i18n/i18n.vcxproj.filters @@ -156,6 +156,9 @@ formatting + + formatting + formatting diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index 416ebcc39c1..9db4848ed33 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -345,6 +345,7 @@ + diff --git a/icu4c/source/i18n/number_capi.cpp b/icu4c/source/i18n/number_capi.cpp index d96da6816e1..41a7b9cf622 100644 --- a/icu4c/source/i18n/number_capi.cpp +++ b/icu4c/source/i18n/number_capi.cpp @@ -20,6 +20,48 @@ using namespace icu::number; using namespace icu::number::impl; +U_NAMESPACE_BEGIN +namespace number { +namespace impl { + +/** + * Implementation class for UNumberFormatter. Wraps a LocalizedNumberFormatter. + */ +struct UNumberFormatterData : public UMemory, + // Magic number as ASCII == "NFR" (NumberFormatteR) + public IcuCApiHelper { + LocalizedNumberFormatter fFormatter; +}; + +struct UFormattedNumberImpl; + +// Magic number as ASCII == "FDN" (FormatteDNumber) +typedef IcuCApiHelper UFormattedNumberApiHelper; + +struct UFormattedNumberImpl : public UFormattedValueImpl, public UFormattedNumberApiHelper { + UFormattedNumberImpl(); + ~UFormattedNumberImpl(); + + FormattedNumber fImpl; + UFormattedNumberData fData; +}; + +UFormattedNumberImpl::UFormattedNumberImpl() + : fImpl(&fData) { + fFormattedValue = &fImpl; +} + +UFormattedNumberImpl::~UFormattedNumberImpl() { + // Disown the data from fImpl so it doesn't get deleted twice + fImpl.fResults = nullptr; +} + +} +} +U_NAMESPACE_END + + + U_CAPI UNumberFormatter* U_EXPORT2 unumf_openForSkeletonAndLocale(const UChar* skeleton, int32_t skeletonLen, const char* locale, UErrorCode* ec) { @@ -36,55 +78,63 @@ unumf_openForSkeletonAndLocale(const UChar* skeleton, int32_t skeletonLen, const U_CAPI UFormattedNumber* U_EXPORT2 unumf_openResult(UErrorCode* ec) { - auto* impl = new UFormattedNumberData(); + auto* impl = new UFormattedNumberImpl(); if (impl == nullptr) { *ec = U_MEMORY_ALLOCATION_ERROR; return nullptr; } - return impl->exportForC(); + return static_cast(impl)->exportForC(); } U_CAPI void U_EXPORT2 unumf_formatInt(const UNumberFormatter* uformatter, int64_t value, UFormattedNumber* uresult, UErrorCode* ec) { const UNumberFormatterData* formatter = UNumberFormatterData::validate(uformatter, *ec); - UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return; } - result->string.clear(); - result->quantity.setToLong(value); - formatter->fFormatter.formatImpl(result, *ec); + result->fData.string.clear(); + result->fData.quantity.setToLong(value); + formatter->fFormatter.formatImpl(&result->fData, *ec); } U_CAPI void U_EXPORT2 unumf_formatDouble(const UNumberFormatter* uformatter, double value, UFormattedNumber* uresult, UErrorCode* ec) { const UNumberFormatterData* formatter = UNumberFormatterData::validate(uformatter, *ec); - UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return; } - result->string.clear(); - result->quantity.setToDouble(value); - formatter->fFormatter.formatImpl(result, *ec); + result->fData.string.clear(); + result->fData.quantity.setToDouble(value); + formatter->fFormatter.formatImpl(&result->fData, *ec); } U_CAPI void U_EXPORT2 unumf_formatDecimal(const UNumberFormatter* uformatter, const char* value, int32_t valueLen, UFormattedNumber* uresult, UErrorCode* ec) { const UNumberFormatterData* formatter = UNumberFormatterData::validate(uformatter, *ec); - UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return; } - result->string.clear(); - result->quantity.setToDecNumber({value, valueLen}, *ec); + result->fData.string.clear(); + result->fData.quantity.setToDecNumber({value, valueLen}, *ec); if (U_FAILURE(*ec)) { return; } - formatter->fFormatter.formatImpl(result, *ec); + formatter->fFormatter.formatImpl(&result->fData, *ec); +} + +U_DRAFT const UFormattedValue* U_EXPORT2 +unumf_resultAsFormattedValue(const UFormattedNumber* uresult, UErrorCode* ec) { + const auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); + if (U_FAILURE(*ec)) { return nullptr; } + + return static_cast(result)->exportConstForC(); } U_CAPI int32_t U_EXPORT2 unumf_resultToString(const UFormattedNumber* uresult, UChar* buffer, int32_t bufferCapacity, UErrorCode* ec) { - const UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + const auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return 0; } if (buffer == nullptr ? bufferCapacity != 0 : bufferCapacity < 0) { @@ -92,12 +142,12 @@ unumf_resultToString(const UFormattedNumber* uresult, UChar* buffer, int32_t buf return 0; } - return result->string.toTempUnicodeString().extract(buffer, bufferCapacity, *ec); + return result->fImpl.toTempString(*ec).extract(buffer, bufferCapacity, *ec); } U_CAPI UBool U_EXPORT2 unumf_resultNextFieldPosition(const UFormattedNumber* uresult, UFieldPosition* ufpos, UErrorCode* ec) { - const UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + const auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return FALSE; } if (ufpos == nullptr) { @@ -109,7 +159,7 @@ unumf_resultNextFieldPosition(const UFormattedNumber* uresult, UFieldPosition* u fp.setField(ufpos->field); fp.setBeginIndex(ufpos->beginIndex); fp.setEndIndex(ufpos->endIndex); - bool retval = result->string.nextFieldPosition(fp, *ec); + bool retval = result->fImpl.nextFieldPosition(fp, *ec); ufpos->beginIndex = fp.getBeginIndex(); ufpos->endIndex = fp.getEndIndex(); // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool @@ -119,7 +169,7 @@ unumf_resultNextFieldPosition(const UFormattedNumber* uresult, UFieldPosition* u U_CAPI void U_EXPORT2 unumf_resultGetAllFieldPositions(const UFormattedNumber* uresult, UFieldPositionIterator* ufpositer, UErrorCode* ec) { - const UFormattedNumberData* result = UFormattedNumberData::validate(uresult, *ec); + const auto* result = UFormattedNumberApiHelper::validate(uresult, *ec); if (U_FAILURE(*ec)) { return; } if (ufpositer == nullptr) { @@ -128,14 +178,13 @@ unumf_resultGetAllFieldPositions(const UFormattedNumber* uresult, UFieldPosition } auto* fpi = reinterpret_cast(ufpositer); - FieldPositionIteratorHandler fpih(fpi, *ec); - result->string.getAllFieldPositions(fpih, *ec); + result->fImpl.getAllFieldPositions(*fpi, *ec); } U_CAPI void U_EXPORT2 unumf_closeResult(UFormattedNumber* uresult) { UErrorCode localStatus = U_ZERO_ERROR; - const UFormattedNumberData* impl = UFormattedNumberData::validate(uresult, localStatus); + const UFormattedNumberImpl* impl = UFormattedNumberApiHelper::validate(uresult, localStatus); delete impl; } diff --git a/icu4c/source/i18n/number_fluent.cpp b/icu4c/source/i18n/number_fluent.cpp index a66e3bd0f23..320fcbe40ca 100644 --- a/icu4c/source/i18n/number_fluent.cpp +++ b/icu4c/source/i18n/number_fluent.cpp @@ -670,6 +670,10 @@ void LocalizedNumberFormatter::formatImpl(impl::UFormattedNumberData* results, U } else { NumberFormatterImpl::formatStatic(fMacros, results->quantity, results->string, status); } + if (U_FAILURE(status)) { + return; + } + results->string.writeTerminator(status); } void LocalizedNumberFormatter::getAffixImpl(bool isPrefix, bool isNegative, UnicodeString& result, @@ -784,6 +788,17 @@ UnicodeString FormattedNumber::toString(UErrorCode& status) const { return fResults->string.toUnicodeString(); } +UnicodeString FormattedNumber::toTempString(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->string.toTempUnicodeString(); +} + Appendable& FormattedNumber::appendTo(Appendable& appendable) { UErrorCode localStatus = U_ZERO_ERROR; return appendTo(appendable, localStatus); @@ -801,6 +816,18 @@ Appendable& FormattedNumber::appendTo(Appendable& appendable, UErrorCode& status return appendable; } +UBool FormattedNumber::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const { + if (U_FAILURE(status)) { + return FALSE; + } + if (fResults == nullptr) { + status = fErrorCode; + return FALSE; + } + // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool + return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; +} + void FormattedNumber::populateFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) { if (U_FAILURE(status)) { return; diff --git a/icu4c/source/i18n/number_stringbuilder.cpp b/icu4c/source/i18n/number_stringbuilder.cpp index bdca5418473..f756edc28e6 100644 --- a/icu4c/source/i18n/number_stringbuilder.cpp +++ b/icu4c/source/i18n/number_stringbuilder.cpp @@ -33,7 +33,15 @@ inline void uprv_memmove2(void* dest, const void* src, size_t len) { } // namespace -NumberStringBuilder::NumberStringBuilder() = default; +NumberStringBuilder::NumberStringBuilder() { +#if U_DEBUG + // Initializing the memory to non-zero helps catch some bugs that involve + // reading from an improperly terminated string. + for (int32_t i=0; i= 0); U_ASSERT(index <= fLength); @@ -428,81 +446,107 @@ bool NumberStringBuilder::nextFieldPosition(FieldPosition& fp, UErrorCode& statu return FALSE; } - auto field = static_cast(rawField); + ConstrainedFieldPosition cfpos; + cfpos.constrainField(UFIELD_CATEGORY_NUMBER, rawField); + cfpos.setState(UFIELD_CATEGORY_NUMBER, rawField, fp.getBeginIndex(), fp.getEndIndex()); + if (nextPosition(cfpos, status)) { + fp.setBeginIndex(cfpos.getStart()); + fp.setEndIndex(cfpos.getLimit()); + return true; + } - bool seenStart = false; - int32_t fractionStart = -1; - int32_t startIndex = fp.getEndIndex(); - for (int32_t i = fZero + startIndex; i <= fZero + fLength; i++) { - Field _field = UNUM_FIELD_COUNT; - if (i < fZero + fLength) { - _field = getFieldPtr()[i]; - } - if (seenStart && field != _field) { - // Special case: GROUPING_SEPARATOR counts as an INTEGER. - if (field == UNUM_INTEGER_FIELD && _field == UNUM_GROUPING_SEPARATOR_FIELD) { - continue; - } - fp.setEndIndex(i - fZero); - // Trim ignorables (whitespace, etc.) from the edge of the field. - UFieldPosition ufp = {0, fp.getBeginIndex(), fp.getEndIndex()}; - if (trimFieldPosition(ufp)) { - fp.setBeginIndex(ufp.beginIndex); - fp.setEndIndex(ufp.endIndex); + // Special case: fraction should start after integer if fraction is not present + if (rawField == UNUM_FRACTION_FIELD && fp.getEndIndex() == 0) { + bool inside = false; + int32_t i = fZero; + for (; i < fZero + fLength; i++) { + if (isIntOrGroup(getFieldPtr()[i]) || getFieldPtr()[i] == UNUM_DECIMAL_SEPARATOR_FIELD) { + inside = true; + } else if (inside) { break; } - // This position was all ignorables; continue to the next position. - fp.setEndIndex(fp.getBeginIndex()); - seenStart = false; - } else if (!seenStart && field == _field) { - fp.setBeginIndex(i - fZero); - seenStart = true; - } - if (_field == UNUM_INTEGER_FIELD || _field == UNUM_DECIMAL_SEPARATOR_FIELD) { - fractionStart = i - fZero + 1; } + fp.setBeginIndex(i - fZero); + fp.setEndIndex(i - fZero); } - // Backwards compatibility: FRACTION needs to start after INTEGER if empty. - // Do not return that a field was found, though, since there is not actually a fraction part. - if (field == UNUM_FRACTION_FIELD && !seenStart && fractionStart != -1) { - fp.setBeginIndex(fractionStart); - fp.setEndIndex(fractionStart); - } - - return seenStart; + return false; } void NumberStringBuilder::getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const { - Field current = UNUM_FIELD_COUNT; - int32_t currentStart = -1; - for (int32_t i = 0; i < fLength; i++) { - Field field = fieldAt(i); - if (current == UNUM_INTEGER_FIELD && field == UNUM_GROUPING_SEPARATOR_FIELD) { - // Special case: GROUPING_SEPARATOR counts as an INTEGER. - // TODO(ICU-13064): Grouping separator can be more than 1 code unit. - fpih.addAttribute(UNUM_GROUPING_SEPARATOR_FIELD, i, i + 1); - } else if (current != field) { - if (current != UNUM_FIELD_COUNT) { - UFieldPosition fp = {0, currentStart, i}; - if (trimFieldPosition(fp)) { - fpih.addAttribute(current, fp.beginIndex, fp.endIndex); + ConstrainedFieldPosition cfpos; + while (nextPosition(cfpos, status)) { + fpih.addAttribute(cfpos.getField(), cfpos.getStart(), cfpos.getLimit()); + } +} + +bool NumberStringBuilder::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& /*status*/) const { + bool isSearchingForField = false; + if (cfpos.getConstraintType() == UCFPOS_CONSTRAINT_CATEGORY) { + if (cfpos.getCategory() != UFIELD_CATEGORY_NUMBER) { + return false; + } + } else if (cfpos.getConstraintType() == UCFPOS_CONSTRAINT_FIELD) { + isSearchingForField = true; + } + + int32_t fieldStart = -1; + int32_t currField = UNUM_FIELD_COUNT; + for (int32_t i = fZero + cfpos.getLimit(); i <= fZero + fLength; i++) { + Field _field = (i < fZero + fLength) ? getFieldPtr()[i] : UNUM_FIELD_COUNT; + // Case 1: currently scanning a field. + if (currField != UNUM_FIELD_COUNT) { + if (currField != _field) { + int32_t end = i - fZero; + // Grouping separators can be whitespace; don't throw them out! + if (currField != UNUM_GROUPING_SEPARATOR_FIELD) { + end = trimBack(i - fZero); } + if (end <= fieldStart) { + // Entire field position is ignorable; skip. + fieldStart = -1; + currField = UNUM_FIELD_COUNT; + i--; // look at this index again + continue; + } + int32_t start = fieldStart; + if (currField != UNUM_GROUPING_SEPARATOR_FIELD) { + start = trimFront(start); + } + cfpos.setState(UFIELD_CATEGORY_NUMBER, currField, start, end); + return true; } - current = field; - currentStart = i; + continue; } - if (U_FAILURE(status)) { - return; - } - } - if (current != UNUM_FIELD_COUNT) { - UFieldPosition fp = {0, currentStart, fLength}; - if (trimFieldPosition(fp)) { - fpih.addAttribute(current, fp.beginIndex, fp.endIndex); + // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER. + if ((!isSearchingForField || cfpos.getField() == UNUM_INTEGER_FIELD) + && i > fZero + && i - fZero > cfpos.getLimit() // don't return the same field twice in a row + && isIntOrGroup(getFieldPtr()[i - 1]) + && !isIntOrGroup(_field)) { + int j = i - 1; + for (; j >= fZero && isIntOrGroup(getFieldPtr()[j]); j--) {} + cfpos.setState(UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, j - fZero + 1, i - fZero); + return true; + } + // Special case: skip over INTEGER; will be coalesced later. + if (_field == UNUM_INTEGER_FIELD) { + _field = UNUM_FIELD_COUNT; + } + // Case 2: no field starting at this position. + if (_field == UNUM_FIELD_COUNT) { + continue; + } + // Case 3: check for field starting at this position + if (!isSearchingForField || cfpos.getField() == _field) { + fieldStart = i - fZero; + currField = _field; } } + + U_ASSERT(currField == UNUM_FIELD_COUNT); + return false; } bool NumberStringBuilder::containsField(Field field) const { @@ -514,26 +558,23 @@ bool NumberStringBuilder::containsField(Field field) const { return false; } -bool NumberStringBuilder::trimFieldPosition(UFieldPosition& fp) const { - // Trim ignorables from the back - int32_t endIgnorablesRelPos = unisets::get(unisets::DEFAULT_IGNORABLES)->spanBack( - getCharPtr() + fZero + fp.beginIndex, - fp.endIndex - fp.beginIndex, - USET_SPAN_CONTAINED); +bool NumberStringBuilder::isIntOrGroup(Field field) { + return field == UNUM_INTEGER_FIELD + || field ==UNUM_GROUPING_SEPARATOR_FIELD; +} - // Check if the entire segment is ignorables - if (endIgnorablesRelPos == 0) { - return false; - } - fp.endIndex = fp.beginIndex + endIgnorablesRelPos; - - // Trim ignorables from the front - int32_t startIgnorablesRelPos = unisets::get(unisets::DEFAULT_IGNORABLES)->span( - getCharPtr() + fZero + fp.beginIndex, - fp.endIndex - fp.beginIndex, +int32_t NumberStringBuilder::trimBack(int32_t limit) const { + return unisets::get(unisets::DEFAULT_IGNORABLES)->spanBack( + getCharPtr() + fZero, + limit, + USET_SPAN_CONTAINED); +} + +int32_t NumberStringBuilder::trimFront(int32_t start) const { + return start + unisets::get(unisets::DEFAULT_IGNORABLES)->span( + getCharPtr() + fZero + start, + fLength - start, USET_SPAN_CONTAINED); - fp.beginIndex = fp.beginIndex + startIgnorablesRelPos; - return true; } #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/number_stringbuilder.h b/icu4c/source/i18n/number_stringbuilder.h index e2a5f9a6d80..ca3d6be417f 100644 --- a/icu4c/source/i18n/number_stringbuilder.h +++ b/icu4c/source/i18n/number_stringbuilder.h @@ -85,6 +85,8 @@ class U_I18N_API NumberStringBuilder : public UMemory { int32_t insert(int32_t index, const NumberStringBuilder &other, UErrorCode &status); + void writeTerminator(UErrorCode& status); + /** * Gets a "safe" UnicodeString that can be used even after the NumberStringBuilder is destructed. * */ @@ -106,6 +108,8 @@ class U_I18N_API NumberStringBuilder : public UMemory { void getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const; + bool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const; + bool containsField(Field field) const; private: @@ -141,7 +145,11 @@ class U_I18N_API NumberStringBuilder : public UMemory { int32_t remove(int32_t index, int32_t count); - bool trimFieldPosition(UFieldPosition& fpos) const; + static bool isIntOrGroup(Field field); + + int32_t trimBack(int32_t limit) const; + + int32_t trimFront(int32_t start) const; }; } // namespace impl diff --git a/icu4c/source/i18n/number_utypes.h b/icu4c/source/i18n/number_utypes.h index 303f82e6a95..18917dea514 100644 --- a/icu4c/source/i18n/number_utypes.h +++ b/icu4c/source/i18n/number_utypes.h @@ -17,28 +17,26 @@ U_NAMESPACE_BEGIN namespace number { namespace impl { -/** - * Implementation class for UNumberFormatter. Wraps a LocalizedNumberFormatter. - */ -struct UNumberFormatterData : public UMemory, - // Magic number as ASCII == "NFR" (NumberFormatteR) - public IcuCApiHelper { - LocalizedNumberFormatter fFormatter; +struct UFormattedValueImpl; + +// Magic number as ASCII == "UFV" +typedef IcuCApiHelper UFormattedValueApiHelper; + +struct UFormattedValueImpl : public UMemory, public UFormattedValueApiHelper { + FormattedValue* fFormattedValue = nullptr; }; /** - * Implementation class for UFormattedNumber. + * Struct for data used by FormattedNumber. * - * This struct is also held internally by the C++ version FormattedNumber since the member types are not + * This struct is held internally by the C++ version FormattedNumber since the member types are not * declared in the public header file. * * The DecimalQuantity is not currently being used by FormattedNumber, but at some point it could be used * to add a toDecNumber() or similar method. */ -struct UFormattedNumberData : public UMemory, - // Magic number as ASCII == "FDN" (FormatteDNumber) - public IcuCApiHelper { +struct UFormattedNumberData : public UMemory { DecimalQuantity quantity; NumberStringBuilder string; }; diff --git a/icu4c/source/i18n/numrange_fluent.cpp b/icu4c/source/i18n/numrange_fluent.cpp index 12b006c8ad5..27ffd0b6bc5 100644 --- a/icu4c/source/i18n/numrange_fluent.cpp +++ b/icu4c/source/i18n/numrange_fluent.cpp @@ -318,6 +318,10 @@ void LocalizedNumberRangeFormatter::formatImpl( return; } impl->format(results, equalBeforeRounding, status); + if (U_FAILURE(status)) { + return; + } + results.string.writeTerminator(status); } const impl::NumberRangeFormatterImpl* @@ -389,6 +393,17 @@ UnicodeString FormattedNumberRange::toString(UErrorCode& status) const { return fResults->string.toUnicodeString(); } +UnicodeString FormattedNumberRange::toTempString(UErrorCode& status) const { + if (U_FAILURE(status)) { + return ICU_Utility::makeBogusString(); + } + if (fResults == nullptr) { + status = fErrorCode; + return ICU_Utility::makeBogusString(); + } + return fResults->string.toTempUnicodeString(); +} + Appendable& FormattedNumberRange::appendTo(Appendable& appendable, UErrorCode& status) const { if (U_FAILURE(status)) { return appendable; @@ -401,6 +416,18 @@ Appendable& FormattedNumberRange::appendTo(Appendable& appendable, UErrorCode& s return appendable; } +UBool FormattedNumberRange::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const { + if (U_FAILURE(status)) { + return FALSE; + } + if (fResults == nullptr) { + status = fErrorCode; + return FALSE; + } + // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool + return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; +} + UBool FormattedNumberRange::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const { if (U_FAILURE(status)) { return FALSE; diff --git a/icu4c/source/i18n/unicode/formattedvalue.h b/icu4c/source/i18n/unicode/formattedvalue.h new file mode 100644 index 00000000000..54593d60689 --- /dev/null +++ b/icu4c/source/i18n/unicode/formattedvalue.h @@ -0,0 +1,314 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#ifndef __FORMATTEDVALUE_H__ +#define __FORMATTEDVALUE_H__ + +#include "unicode/utypes.h" +#if !UCONFIG_NO_FORMATTING + +#include "unicode/appendable.h" +#include "unicode/fpositer.h" +#include "unicode/unistr.h" +#include "unicode/uformattedvalue.h" + +U_NAMESPACE_BEGIN + +/** + * \file + * \brief C++ API: Abstract operations for localized strings. + * + * This file contains declarations for classes that deal with formatted strings. A number + * of APIs throughout ICU use these classes for expressing their localized output. + */ + + +/** + * Represents a span of a string containing a given field. + * + * This class differs from FieldPosition in the following ways: + * + * 1. It has information on the field category. + * 2. It allows you to set constraints to use when iterating over field positions. + * 3. It is used for the newer FormattedValue APIs. + * + * This class is not intended for public subclassing. + * + * @draft ICU 64 + */ +class U_I18N_API ConstrainedFieldPosition : public UMemory { + public: + + /** + * Initializes a ConstrainedFieldPosition. + * + * By default, the ConstrainedFieldPosition has no iteration constraints. + * + * @draft ICU 64 + */ + ConstrainedFieldPosition(); + + /** @draft ICU 64 */ + ~ConstrainedFieldPosition(); + + /** + * Resets this ConstrainedFieldPosition to its initial state, as if it were newly created: + * + * - Removes any constraints that may have been set on the instance. + * - Resets the iteration position. + * + * @draft ICU 64 + */ + void reset(); + + /** + * Sets a constraint on the field category. + * + * When this instance of ConstrainedFieldPosition is passed to FormattedValue#nextPosition, + * positions are skipped unless they have the given category. + * + * Any previously set constraints are cleared. + * + * For example, to loop over only the number-related fields: + * + * ConstrainedFieldPosition cfpos; + * cfpos.constrainCategory(UFIELDCATEGORY_NUMBER_FORMAT); + * while (fmtval.nextPosition(cfpos, status)) { + * // handle the number-related field position + * } + * + * Changing the constraint while in the middle of iterating over a FormattedValue + * does not generally have well-defined behavior. + * + * @param category The field category to fix when iterating. + * @draft ICU 64 + */ + void constrainCategory(UFieldCategory category); + + /** + * Sets a constraint on the category and field. + * + * When this instance of ConstrainedFieldPosition is passed to FormattedValue#nextPosition, + * positions are skipped unless they have the given category and field. + * + * Any previously set constraints are cleared. + * + * For example, to loop over all grouping separators: + * + * ConstrainedFieldPosition cfpos; + * cfpos.constrainField(UFIELDCATEGORY_NUMBER_FORMAT, UNUM_GROUPING_SEPARATOR_FIELD); + * while (fmtval.nextPosition(cfpos, status)) { + * // handle the grouping separator position + * } + * + * Changing the constraint while in the middle of iterating over a FormattedValue + * does not generally have well-defined behavior. + * + * @param category The field category to fix when iterating. + * @param field The field to fix when iterating. + * @draft ICU 64 + */ + void constrainField(UFieldCategory category, int32_t field); + + /** + * Gets the currently active constraint. + * + * @return The currently active constraint type. + * @draft ICU 64 + */ + inline UCFPosConstraintType getConstraintType() const { + return fConstraint; + } + + /** + * Gets the field category for the current position. + * + * If a category or field constraint was set, this function returns the constrained + * category. Otherwise, the return value is well-defined only after + * FormattedValue#nextPosition returns TRUE. + * + * @return The field category saved in the instance. + * @draft ICU 64 + */ + inline UFieldCategory getCategory() const { + return fCategory; + }; + + /** + * Gets the field for the current position. + * + * If a field constraint was set, this function returns the constrained + * field. Otherwise, the return value is well-defined only after + * FormattedValue#nextPosition returns TRUE. + * + * @return The field saved in the instance. + * @draft ICU 64 + */ + inline int32_t getField() const { + return fField; + }; + + /** + * Gets the INCLUSIVE start index for the current position. + * + * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. + * + * @return The start index saved in the instance. + * @draft ICU 64 + */ + inline int32_t getStart() const { + return fStart; + }; + + /** + * Gets the EXCLUSIVE end index stored for the current position. + * + * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. + * + * @return The end index saved in the instance. + * @draft ICU 64 + */ + inline int32_t getLimit() const { + return fLimit; + }; + + //////////////////////////////////////////////////////////////////// + //// The following methods are for FormattedValue implementers; //// + //// most users can ignore them. //// + //////////////////////////////////////////////////////////////////// + + /** + * Gets an int64 that FormattedValue implementations may use for storage. + * + * The initial value is zero. + * + * Users of FormattedValue should not need to call this method. + * + * @return The current iteration context from {@link #setInt64IterationContext}. + * @draft ICU 64 + */ + inline int64_t getInt64IterationContext() const { + return fContext; + } + + /** + * Sets an int64 that FormattedValue implementations may use for storage. + * + * Intended to be used by FormattedValue implementations. + * + * @param context The new iteration context. + * @draft ICU 64 + */ + void setInt64IterationContext(int64_t context); + + /** + * Sets new values for the primary public getters. + * + * Intended to be used by FormattedValue implementations. + * + * It is up to the implementation to ensure that the user-requested + * constraints are satisfied. This method does not check! + * + * @param category The new field category. + * @param field The new field. + * @param start The new inclusive start index. + * @param limit The new exclusive end index. + * @draft ICU 64 + */ + void setState( + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit); + + private: + int64_t fContext = 0LL; + int32_t fField = 0; + int32_t fStart = 0; + int32_t fLimit = 0; + UCFPosConstraintType fConstraint = UCFPOS_CONSTRAINT_NONE; + UFieldCategory fCategory = UFIELD_CATEGORY_UNDEFINED; +}; + + +/** + * An abstract formatted value: a string with associated field attributes. + * Many formatters format to classes implementing FormattedValue. + * + * @draft ICU 64 + */ +class U_I18N_API FormattedValue /* not : public UObject because this is an interface/mixin class */ { + public: + virtual ~FormattedValue(); + + /** + * Returns the formatted string as a self-contained UnicodeString. + * + * If you need the string within the current scope only, consider #toTempString. + * + * @param status Set if an error occurs. + * @return a UnicodeString containing the formatted string. + * + * @draft ICU 64 + */ + virtual UnicodeString toString(UErrorCode& status) const = 0; + + /** + * Returns the formatted string as a read-only alias to memory owned by the FormattedValue. + * + * The return value is valid only as long as this FormattedValue is present and unchanged in + * memory. If you need the string outside the current scope, consider #toString. + * + * The buffer returned by calling UnicodeString#getBuffer() on the return value is + * guaranteed to be NUL-terminated. + * + * @param status Set if an error occurs. + * @return a temporary UnicodeString containing the formatted string. + * + * @draft ICU 64 + */ + virtual UnicodeString toTempString(UErrorCode& status) const = 0; + + /** + * Appends the formatted string to an Appendable. + * + * @param appendable + * The Appendable to which to append the string output. + * @param status Set if an error occurs. + * @return The same Appendable, for chaining. + * + * @draft ICU 64 + * @see Appendable + */ + virtual Appendable& appendTo(Appendable& appendable, UErrorCode& status) const = 0; + + /** + * Iterates over field positions in the FormattedValue. This lets you determine the position + * of specific types of substrings, like a month or a decimal separator. + * + * To loop over all field positions: + * + * ConstrainedFieldPosition cfpos; + * while (fmtval.nextPosition(cfpos, status)) { + * // handle the field position; get information from cfpos + * } + * + * @param cfpos + * The object used for iteration state. This can provide constraints to iterate over + * only one specific category or field; + * see ConstrainedFieldPosition#constrainCategory + * and ConstrainedFieldPosition#constrainField. + * @param status Set if an error occurs. + * @return TRUE if a new occurrence of the field was found; + * FALSE otherwise or if an error was set. + * + * @draft ICU 64 + */ + virtual UBool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const = 0; +}; + + +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ +#endif // __FORMATTEDVALUE_H__ diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index 229e4bb3ad6..47a04dda3d2 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -11,6 +11,7 @@ #include "unicode/dcfmtsym.h" #include "unicode/currunit.h" #include "unicode/fieldpos.h" +#include "unicode/formattedvalue.h" #include "unicode/fpositer.h" #include "unicode/measunit.h" #include "unicode/nounit.h" @@ -146,6 +147,7 @@ class GeneratorHelpers; class DecNum; class NumberRangeFormatterImpl; struct RangeMacroProps; +struct UFormattedNumberImpl; /** * Used for NumberRangeFormatter and implemented in numrange_fluent.cpp. @@ -2447,8 +2449,45 @@ class U_I18N_API LocalizedNumberFormatter * * @draft ICU 60 */ -class U_I18N_API FormattedNumber : public UMemory { +class U_I18N_API FormattedNumber : public UMemory, public FormattedValue { public: + + /** + * Default constructor; makes an empty FormattedNumber. + */ + FormattedNumber() + : fResults(nullptr), fErrorCode(U_INVALID_STATE_ERROR) {}; + + /** + * Copying not supported; use move constructor instead. + */ + FormattedNumber(const FormattedNumber&) = delete; + + /** + * Move constructor: + * Leaves the source FormattedNumber in an undefined state. + * @draft ICU 62 + */ + FormattedNumber(FormattedNumber&& src) U_NOEXCEPT; + + /** + * Destruct an instance of FormattedNumber, cleaning up any memory it might own. + * @draft ICU 60 + */ + virtual ~FormattedNumber() U_OVERRIDE; + + /** + * Copying not supported; use move assignment instead. + */ + FormattedNumber& operator=(const FormattedNumber&) = delete; + + /** + * Move assignment: + * Leaves the source FormattedNumber in an undefined state. + * @draft ICU 62 + */ + FormattedNumber& operator=(FormattedNumber&& src) U_NOEXCEPT; + #ifndef U_HIDE_DEPRECATED_API /** * Returns a UnicodeString representation of the formatted number. @@ -2462,14 +2501,14 @@ class U_I18N_API FormattedNumber : public UMemory { #endif /* U_HIDE_DEPRECATED_API */ /** - * Returns a UnicodeString representation of the formatted number. - * - * @param status - * Set if an error occurs while formatting the number to the UnicodeString. - * @return a UnicodeString containing the localized number. - * @draft ICU 62 + * @copydoc FormattedValue::toString() */ - UnicodeString toString(UErrorCode& status) const; + UnicodeString toString(UErrorCode& status) const U_OVERRIDE; + + /** + * @copydoc FormattedValue::toTempString() + */ + UnicodeString toTempString(UErrorCode& status) const U_OVERRIDE; #ifndef U_HIDE_DEPRECATED_API /** @@ -2483,21 +2522,18 @@ class U_I18N_API FormattedNumber : public UMemory { * See http://bugs.icu-project.org/trac/ticket/13746 * @see Appendable */ - Appendable &appendTo(Appendable &appendable); + Appendable &appendTo(Appendable& appendable); #endif /* U_HIDE_DEPRECATED_API */ /** - * Appends the formatted number to an Appendable. - * - * @param appendable - * The Appendable to which to append the formatted number string. - * @param status - * Set if an error occurs while formatting the number to the Appendable. - * @return The same Appendable, for chaining. - * @draft ICU 62 - * @see Appendable + * @copydoc FormattedValue::appendTo() */ - Appendable &appendTo(Appendable &appendable, UErrorCode& status) const; + Appendable &appendTo(Appendable& appendable, UErrorCode& status) const U_OVERRIDE; + + /** + * @copydoc FormattedValue::nextPosition() + */ + UBool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const U_OVERRIDE; #ifndef U_HIDE_DEPRECATED_API /** @@ -2606,39 +2642,9 @@ class U_I18N_API FormattedNumber : public UMemory { #endif /* U_HIDE_INTERNAL_API */ - /** - * Copying not supported; use move constructor instead. - */ - FormattedNumber(const FormattedNumber&) = delete; - - /** - * Copying not supported; use move assignment instead. - */ - FormattedNumber& operator=(const FormattedNumber&) = delete; - - /** - * Move constructor: - * Leaves the source FormattedNumber in an undefined state. - * @draft ICU 62 - */ - FormattedNumber(FormattedNumber&& src) U_NOEXCEPT; - - /** - * Move assignment: - * Leaves the source FormattedNumber in an undefined state. - * @draft ICU 62 - */ - FormattedNumber& operator=(FormattedNumber&& src) U_NOEXCEPT; - - /** - * Destruct an instance of FormattedNumber, cleaning up any memory it might own. - * @draft ICU 60 - */ - ~FormattedNumber(); - private: // Can't use LocalPointer because UFormattedNumberData is forward-declared - const impl::UFormattedNumberData *fResults; + impl::UFormattedNumberData *fResults; // Error code for the terminal methods UErrorCode fErrorCode; @@ -2655,6 +2661,9 @@ class U_I18N_API FormattedNumber : public UMemory { // To give LocalizedNumberFormatter format methods access to this class's constructor: friend class LocalizedNumberFormatter; + + // To give C API access to internals + friend struct impl::UFormattedNumberImpl; }; /** diff --git a/icu4c/source/i18n/unicode/numberrangeformatter.h b/icu4c/source/i18n/unicode/numberrangeformatter.h index d5466b12766..dee75c6729f 100644 --- a/icu4c/source/i18n/unicode/numberrangeformatter.h +++ b/icu4c/source/i18n/unicode/numberrangeformatter.h @@ -8,6 +8,7 @@ #include #include "unicode/appendable.h" #include "unicode/fieldpos.h" +#include "unicode/formattedvalue.h" #include "unicode/fpositer.h" #include "unicode/numberformatter.h" @@ -662,30 +663,27 @@ class U_I18N_API LocalizedNumberRangeFormatter * * @draft ICU 63 */ -class U_I18N_API FormattedNumberRange : public UMemory { +class U_I18N_API FormattedNumberRange : public UMemory, public FormattedValue { 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 + * @copydoc FormattedValue::toString() */ - UnicodeString toString(UErrorCode& status) const; + UnicodeString toString(UErrorCode& status) const U_OVERRIDE; /** - * 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 + * @copydoc FormattedValue::toTempString() */ - Appendable &appendTo(Appendable &appendable, UErrorCode& status) const; + UnicodeString toTempString(UErrorCode& status) const U_OVERRIDE; + + /** + * @copydoc FormattedValue::appendTo() + */ + Appendable &appendTo(Appendable &appendable, UErrorCode& status) const U_OVERRIDE; + + /** + * @copydoc FormattedValue::nextPosition() + */ + UBool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const U_OVERRIDE; /** * Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given diff --git a/icu4c/source/i18n/unicode/uformattedvalue.h b/icu4c/source/i18n/unicode/uformattedvalue.h new file mode 100644 index 00000000000..d24ecb2fb2e --- /dev/null +++ b/icu4c/source/i18n/unicode/uformattedvalue.h @@ -0,0 +1,442 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#ifndef __UFORMATTEDVALUE_H__ +#define __UFORMATTEDVALUE_H__ + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "unicode/ufieldpositer.h" + +/** + * \file + * \brief C API: Abstract operations for localized strings. + * + * This file contains declarations for classes that deal with formatted strings. A number + * of APIs throughout ICU use these classes for expressing their localized output. + */ + + +/** + * All possible field categories in ICU. Every entry in this enum corresponds + * to another enum that exists in ICU. + * + * @draft ICU 64 + */ +typedef enum UFieldCategory { + /** + * For an undefined field category. + * + * @draft ICU 64 + */ + UFIELD_CATEGORY_UNDEFINED = 0, + + /** + * For fields in UDateFormatField (udat.h), from ICU 3.0. + * + * @draft ICU 64 + */ + UFIELD_CATEGORY_DATE, + + /** + * For fields in UNumberFormatFields (unum.h), from ICU 49. + * + * @draft ICU 64 + */ + UFIELD_CATEGORY_NUMBER, + + /** + * For fields in UListFormatterField (ulistformatter.h), from ICU 63. + * + * @draft ICU 64 + */ + UFIELD_CATEGORY_LIST, + +} UFieldCategory; + + +/** + * Represents the type of constraint for ConstrainedFieldPosition. + * + * Constraints are used to control the behavior of iteration in FormattedValue. + * + * @draft ICU 64 + */ +typedef enum UCFPosConstraintType { + /** + * Represents the lack of a constraint. + * + * This is the return value of ConstrainedFieldPosition#getConstraintType or + * ucfpos_getConstraintType if no "constrain" methods were called. + * + * @draft ICU 64 + */ + UCFPOS_CONSTRAINT_NONE, + + /** + * Represents that the field category is constrained. + * + * This is the return value of ConstrainedFieldPosition#getConstraintType or + * cfpos_getConstraintType after ConstrainedFieldPosition#constrainCategory or + * cfpos_constrainCategory is called. + * + * Use getCategory to access the category. FormattedValue implementations + * should not change that values while this constraint is active. + * + * @draft ICU 64 + */ + UCFPOS_CONSTRAINT_CATEGORY, + + /** + * Represents that the field and field category are constrained. + * + * This is the return value of ConstrainedFieldPosition#getConstraintType or + * cfpos_getConstraintType after ConstrainedFieldPosition#constrainField or + * cfpos_constrainField is called. + * + * Use getCategory and getField to access the category and field. + * FormattedValue implementations should not change those values while + * this constraint is active. + * + * @draft ICU 64 + */ + UCFPOS_CONSTRAINT_FIELD +} UCFPosConstraintType; + + +struct UConstrainedFieldPosition; +/** + * Represents a span of a string containing a given field. + * + * This struct differs from UFieldPosition in the following ways: + * + * 1. It has information on the field category. + * 2. It allows you to set constraints to use when iterating over field positions. + * 3. It is used for the newer FormattedValue APIs. + * + * @draft ICU 64 + */ +typedef struct UConstrainedFieldPosition UConstrainedFieldPosition; + + +/** + * Creates a new UConstrainedFieldPosition. + * + * By default, the UConstrainedFieldPosition has no iteration constraints. + * + * @param ec Set if an error occurs. + * @return The new object, or NULL if an error occurs. + * @draft ICU 64 + */ +U_DRAFT UConstrainedFieldPosition* U_EXPORT2 +ucfpos_open(UErrorCode* ec); + + +/** + * Resets a UConstrainedFieldPosition to its initial state, as if it were newly created. + * + * Removes any constraints that may have been set on the instance. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_reset( + UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +/** + * Destroys a UConstrainedFieldPosition and releases its memory. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_close(UConstrainedFieldPosition* ucfpos); + + +/** + * Sets a constraint on the field category. + * + * When this instance of UConstrainedFieldPosition is passed to ufmtval_nextPosition, + * positions are skipped unless they have the given category. + * + * Any previously set constraints are cleared. + * + * For example, to loop over only the number-related fields: + * + * UConstrainedFieldPosition* ucfpos = ucfpos_open(ec); + * ucfpos_constrainCategory(ucfpos, UFIELDCATEGORY_NUMBER_FORMAT, ec); + * while (ufmtval_nextPosition(ufmtval, ucfpos, ec)) { + * // handle the number-related field position + * } + * ucfpos_close(ucfpos); + * + * Changing the constraint while in the middle of iterating over a FormattedValue + * does not generally have well-defined behavior. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param category The field category to fix when iterating. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_constrainCategory( + UConstrainedFieldPosition* ucfpos, + UFieldCategory category, + UErrorCode* ec); + + +/** + * Sets a constraint on the category and field. + * + * When this instance of UConstrainedFieldPosition is passed to ufmtval_nextPosition, + * positions are skipped unless they have the given category and field. + * + * Any previously set constraints are cleared. + * + * For example, to loop over all grouping separators: + * + * UConstrainedFieldPosition* ucfpos = ucfpos_open(ec); + * ucfpos_constrainField(ucfpos, UFIELDCATEGORY_NUMBER_FORMAT, UNUM_GROUPING_SEPARATOR_FIELD, ec); + * while (ufmtval_nextPosition(ufmtval, ucfpos, ec)) { + * // handle the grouping separator position + * } + * ucfpos_close(ucfpos); + * + * Changing the constraint while in the middle of iterating over a FormattedValue + * does not generally have well-defined behavior. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param category The field category to fix when iterating. + * @param field The field to fix when iterating. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_constrainField( + UConstrainedFieldPosition* ucfpos, + UFieldCategory category, + int32_t field, + UErrorCode* ec); + + +/** + * Gets the currently active constraint. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param ec Set if an error occurs. + * @return The currently active constraint type. + * @draft ICU 64 + */ +U_DRAFT UCFPosConstraintType U_EXPORT2 +ucfpos_getConstraintType( + const UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +/** + * Gets the field category for the current position. + * + * If a category or field constraint was set, this function returns the constrained + * category. Otherwise, the return value is well-defined only after + * ufmtval_nextPosition returns TRUE. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param ec Set if an error occurs. + * @return The field category saved in the instance. + * @draft ICU 64 + */ +U_DRAFT UFieldCategory U_EXPORT2 +ucfpos_getCategory( + const UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +/** + * Gets the field for the current position. + * + * If a field constraint was set, this function returns the constrained + * field. Otherwise, the return value is well-defined only after + * ufmtval_nextPosition returns TRUE. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param ec Set if an error occurs. + * @return The field saved in the instance. + * @draft ICU 64 + */ +U_DRAFT int32_t U_EXPORT2 +ucfpos_getField( + const UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +/** + * Gets the INCLUSIVE start and EXCLUSIVE end index stored for the current position. + * + * The output values are well-defined only after ufmtval_nextPosition returns TRUE. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param pStart Set to the start index saved in the instance. Ignored if nullptr. + * @param pLimit Set to the end index saved in the instance. Ignored if nullptr. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_getIndexes( + const UConstrainedFieldPosition* ucfpos, + int32_t* pStart, + int32_t* pLimit, + UErrorCode* ec); + + +/** + * Gets an int64 that FormattedValue implementations may use for storage. + * + * The initial value is zero. + * + * Users of FormattedValue should not need to call this method. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param ec Set if an error occurs. + * @return The current iteration context from ucfpos_setInt64IterationContext. + * @draft ICU 64 + */ +U_DRAFT int64_t U_EXPORT2 +ucfpos_getInt64IterationContext( + const UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +/** + * Sets an int64 that FormattedValue implementations may use for storage. + * + * Intended to be used by FormattedValue implementations. + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param context The new iteration context. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_setInt64IterationContext( + UConstrainedFieldPosition* ucfpos, + int64_t context, + UErrorCode* ec); + + +/** + * Sets new values for the primary public getters. + * + * Intended to be used by FormattedValue implementations. + * + * It is up to the implementation to ensure that the user-requested + * constraints are satisfied. This method does not check! + * + * @param ucfpos The instance of UConstrainedFieldPosition. + * @param category The new field category. + * @param field The new field. + * @param start The new inclusive start index. + * @param limit The new exclusive end index. + * @param ec Set if an error occurs. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ucfpos_setState( + UConstrainedFieldPosition* ucfpos, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + UErrorCode* ec); + + +struct UFormattedValue; +/** + * An abstract formatted value: a string with associated field attributes. + * Many formatters format to types compatible with UFormattedValue. + * + * @draft ICU 64 + */ +typedef struct UFormattedValue UFormattedValue; + + +/** + * Returns a pointer to the formatted string. The pointer is owned by the UFormattedValue. The + * return value is valid only as long as the UFormattedValue is present and unchanged in memory. + * + * The return value is NUL-terminated but could contain internal NULs. + * + * @param ufmtval + * The object containing the formatted string and attributes. + * @param pLength Output variable for the length of the string. Ignored if NULL. + * @param ec Set if an error occurs. + * @return A NUL-terminated char16 string owned by the UFormattedValue. + * @draft ICU 64 + */ +U_DRAFT const UChar* U_EXPORT2 +ufmtval_getString( + const UFormattedValue* ufmtval, + int32_t* pLength, + UErrorCode* ec); + + +/** + * Iterates over field positions in the UFormattedValue. This lets you determine the position + * of specific types of substrings, like a month or a decimal separator. + * + * To loop over all field positions: + * + * UConstrainedFieldPosition* ucfpos = ucfpos_open(ec); + * while (ufmtval_nextPosition(ufmtval, ucfpos, ec)) { + * // handle the field position; get information from ucfpos + * } + * ucfpos_close(ucfpos); + * + * @param ufmtval + * The object containing the formatted string and attributes. + * @param ucfpos + * The object used for iteration state; can provide constraints to iterate over only + * one specific category or field; + * see ucfpos_constrainCategory + * and ucfpos_constrainField. + * @param ec Set if an error occurs. + * @return TRUE if another position was found; FALSE otherwise. + * @draft ICU 64 + */ +U_DRAFT UBool U_EXPORT2 +ufmtval_nextPosition( + const UFormattedValue* ufmtval, + UConstrainedFieldPosition* ucfpos, + UErrorCode* ec); + + +#if U_SHOW_CPLUSPLUS_API +U_NAMESPACE_BEGIN + +/** + * \class LocalUConstrainedFieldPositionPointer + * "Smart pointer" class; closes a UConstrainedFieldPosition via ucfpos_close(). + * For most methods see the LocalPointerBase base class. + * + * Usage: + * + * LocalUConstrainedFieldPositionPointer ucfpos(ucfpos_open(ec)); + * // no need to explicitly call ucfpos_close() + * + * @draft ICU 64 + */ +U_DEFINE_LOCAL_OPEN_POINTER(LocalUConstrainedFieldPositionPointer, + UConstrainedFieldPosition, + ucfpos_close); + +U_NAMESPACE_END +#endif // U_SHOW_CPLUSPLUS_API + + +#endif /* #if !UCONFIG_NO_FORMATTING */ +#endif // __UFORMATTEDVALUE_H__ diff --git a/icu4c/source/i18n/unicode/unumberformatter.h b/icu4c/source/i18n/unicode/unumberformatter.h index 5926e0e3a9b..f2e5f1dac47 100644 --- a/icu4c/source/i18n/unicode/unumberformatter.h +++ b/icu4c/source/i18n/unicode/unumberformatter.h @@ -9,6 +9,7 @@ #include "unicode/ufieldpositer.h" #include "unicode/umisc.h" +#include "unicode/uformattedvalue.h" /** @@ -531,6 +532,22 @@ unumf_formatDecimal(const UNumberFormatter* uformatter, const char* value, int32 UFormattedNumber* uresult, UErrorCode* ec); +/** + * Returns a representation of a UFormattedNumber as a UFormattedValue, which can be + * subsequently passed to any API requiring that type. + * + * The returned object is owned by the UFormattedNumber and is valid only as long as the + * UFormattedNumber is present and unchanged in memory. + * + * @param uresult The object containing the formatted number. + * @param ec Set if an error occurs. + * @return A representation of the given UFormattedNumber as a UFormattedValue. + * @draft ICU 64 + */ +U_DRAFT const UFormattedValue* U_EXPORT2 +unumf_resultAsFormattedValue(const UFormattedNumber* uresult, UErrorCode* ec); + + /** * Extracts the result number string out of a UFormattedNumber to a UChar buffer if possible. * If bufferCapacity is greater than the required length, a terminating NUL is written. diff --git a/icu4c/source/test/cintltst/Makefile.in b/icu4c/source/test/cintltst/Makefile.in index 208fb787321..a8038378409 100644 --- a/icu4c/source/test/cintltst/Makefile.in +++ b/icu4c/source/test/cintltst/Makefile.in @@ -55,7 +55,7 @@ hpmufn.o tracetst.o reapits.o uregiontest.o ulistfmttest.o\ utexttst.o ucsdetst.o spooftest.o \ cbiditransformtst.o \ cgendtst.o \ -unumberformattertst.o +unumberformattertst.o uformattedvaluetst.o DEPS = $(OBJECTS:.o=.d) diff --git a/icu4c/source/test/cintltst/calltest.c b/icu4c/source/test/cintltst/calltest.c index 96ea400a4ec..6c5af0614c1 100644 --- a/icu4c/source/test/cintltst/calltest.c +++ b/icu4c/source/test/cintltst/calltest.c @@ -45,7 +45,6 @@ void addUSpoofTest(TestNode** root); #if !UCONFIG_NO_FORMATTING void addGendInfoForTest(TestNode** root); #endif -void addUNumberFormatterTest(TestNode** root); void addAllTests(TestNode** root) { @@ -89,6 +88,5 @@ void addAllTests(TestNode** root) addPUtilTest(root); #if !UCONFIG_NO_FORMATTING addGendInfoForTest(root); - addUNumberFormatterTest(root); #endif } diff --git a/icu4c/source/test/cintltst/cformtst.c b/icu4c/source/test/cintltst/cformtst.c index 6ed740c1c7e..f4e62b73906 100644 --- a/icu4c/source/test/cintltst/cformtst.c +++ b/icu4c/source/test/cintltst/cformtst.c @@ -39,6 +39,8 @@ void addCurrencyTest(TestNode**); void addPluralRulesTest(TestNode**); void addURegionTest(TestNode** root); void addUListFmtTest(TestNode** root); +void addUNumberFormatterTest(TestNode** root); +void addUFormattedValueTest(TestNode** root); void addFormatTest(TestNode** root); @@ -61,6 +63,8 @@ void addFormatTest(TestNode** root) addPluralRulesTest(root); addURegionTest(root); addUListFmtTest(root); + addUNumberFormatterTest(root); + addUFormattedValueTest(root); } /*Internal functions used*/ diff --git a/icu4c/source/test/cintltst/cformtst.h b/icu4c/source/test/cintltst/cformtst.h index a5b6bbe2224..e7c233d525d 100644 --- a/icu4c/source/test/cintltst/cformtst.h +++ b/icu4c/source/test/cintltst/cformtst.h @@ -25,11 +25,24 @@ #include "cintltst.h" #include "unicode/udat.h" +#include "unicode/uformattedvalue.h" /* Internal fucntion used by all the test format files */ UChar* myDateFormat(UDateFormat *dat, UDate d); + +// The following is implemented in uformattedvaluetest.c +// TODO: When needed, add overload with a different category for each position +void checkFormattedValue( + const char* message, + const UFormattedValue* fv, + const UChar* expectedString, + UFieldCategory expectedCategory, + const UFieldPosition* expectedFieldPositions, + int32_t expectedFieldPositionsLength); + + #endif /* #if !UCONFIG_NO_FORMATTING */ #endif diff --git a/icu4c/source/test/cintltst/cintltst.vcxproj b/icu4c/source/test/cintltst/cintltst.vcxproj index dd040a69cba..bb5a2a7efee 100644 --- a/icu4c/source/test/cintltst/cintltst.vcxproj +++ b/icu4c/source/test/cintltst/cintltst.vcxproj @@ -243,6 +243,7 @@ + diff --git a/icu4c/source/test/cintltst/cintltst.vcxproj.filters b/icu4c/source/test/cintltst/cintltst.vcxproj.filters index ff185c66e6a..b541163663e 100644 --- a/icu4c/source/test/cintltst/cintltst.vcxproj.filters +++ b/icu4c/source/test/cintltst/cintltst.vcxproj.filters @@ -222,6 +222,9 @@ formatting + + formatting + locales & resources diff --git a/icu4c/source/test/cintltst/uformattedvaluetst.c b/icu4c/source/test/cintltst/uformattedvaluetst.c new file mode 100644 index 00000000000..173ab6e092d --- /dev/null +++ b/icu4c/source/test/cintltst/uformattedvaluetst.c @@ -0,0 +1,192 @@ +// © 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 "unicode/uformattedvalue.h" +#include "unicode/unum.h" +#include "unicode/ustring.h" +#include "cformtst.h" +#include "cintltst.h" +#include "cmemory.h" +#include "cstring.h" +#include "uassert.h" + +static void TestBasic(void); +static void TestSetters(void); + +static void AssertAllPartsEqual( + const char* messagePrefix, + const UConstrainedFieldPosition* ucfpos, + UCFPosConstraintType constraint, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + int64_t context); + +void addUFormattedValueTest(TestNode** root); + +#define TESTCASE(x) addTest(root, &x, "tsformat/uformattedvalue/" #x) + +void addUFormattedValueTest(TestNode** root) { + TESTCASE(TestBasic); + TESTCASE(TestSetters); +} + + +static void TestBasic() { + UErrorCode status = U_ZERO_ERROR; + UConstrainedFieldPosition* ucfpos = ucfpos_open(&status); + assertSuccess("opening ucfpos", &status); + assertTrue("ucfpos should not be null", ucfpos != NULL); + + AssertAllPartsEqual( + "basic", + ucfpos, + UCFPOS_CONSTRAINT_NONE, + UFIELD_CATEGORY_UNDEFINED, + 0, + 0, + 0, + 0LL); + + ucfpos_close(ucfpos); +} + +void TestSetters() { + UErrorCode status = U_ZERO_ERROR; + UConstrainedFieldPosition* ucfpos = ucfpos_open(&status); + assertSuccess("opening ucfpos", &status); + assertTrue("ucfpos should not be null", ucfpos != NULL); + + ucfpos_constrainCategory(ucfpos, UFIELD_CATEGORY_DATE, &status); + assertSuccess("setters 0", &status); + AssertAllPartsEqual( + "setters 0", + ucfpos, + UCFPOS_CONSTRAINT_CATEGORY, + UFIELD_CATEGORY_DATE, + 0, + 0, + 0, + 0LL); + + ucfpos_constrainField(ucfpos, UFIELD_CATEGORY_NUMBER, UNUM_COMPACT_FIELD, &status); + assertSuccess("setters 1", &status); + AssertAllPartsEqual( + "setters 1", + ucfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 0, + 0, + 0LL); + + ucfpos_setInt64IterationContext(ucfpos, 42424242424242LL, &status); + assertSuccess("setters 2", &status); + AssertAllPartsEqual( + "setters 2", + ucfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 0, + 0, + 42424242424242LL); + + ucfpos_setState(ucfpos, UFIELD_CATEGORY_NUMBER, UNUM_COMPACT_FIELD, 5, 10, &status); + assertSuccess("setters 3", &status); + AssertAllPartsEqual( + "setters 3", + ucfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 5, + 10, + 42424242424242LL); + + ucfpos_reset(ucfpos, &status); + assertSuccess("setters 4", &status); + AssertAllPartsEqual( + "setters 4", + ucfpos, + UCFPOS_CONSTRAINT_NONE, + UFIELD_CATEGORY_UNDEFINED, + 0, + 0, + 0, + 0LL); + + ucfpos_close(ucfpos); +} + +static void AssertAllPartsEqual( + const char* messagePrefix, + const UConstrainedFieldPosition* ucfpos, + UCFPosConstraintType constraint, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + int64_t context) { + + UErrorCode status = U_ZERO_ERROR; + + char message[256]; + uprv_strncpy(message, messagePrefix, 256); + int32_t prefixEnd = uprv_strlen(messagePrefix); + message[prefixEnd++] = ':'; + message[prefixEnd++] = ' '; + U_ASSERT(prefixEnd < 256); + +#define AAPE_MSG(suffix) (uprv_strncpy(message+prefixEnd, suffix, 256-prefixEnd)-prefixEnd) + + UCFPosConstraintType _constraintType = ucfpos_getConstraintType(ucfpos, &status); + assertSuccess(AAPE_MSG("constraint"), &status); + assertIntEquals(AAPE_MSG("constraint"), constraint, _constraintType); + + UFieldCategory _category = ucfpos_getCategory(ucfpos, &status); + assertSuccess(AAPE_MSG("_"), &status); + assertIntEquals(AAPE_MSG("category"), category, _category); + + int32_t _field = ucfpos_getField(ucfpos, &status); + assertSuccess(AAPE_MSG("field"), &status); + assertIntEquals(AAPE_MSG("field"), field, _field); + + int32_t _start, _limit; + ucfpos_getIndexes(ucfpos, &_start, &_limit, &status); + assertSuccess(AAPE_MSG("indexes"), &status); + assertIntEquals(AAPE_MSG("start"), start, _start); + assertIntEquals(AAPE_MSG("limit"), limit, _limit); + + int64_t _context = ucfpos_getInt64IterationContext(ucfpos, &status); + assertSuccess(AAPE_MSG("context"), &status); + assertIntEquals(AAPE_MSG("context"), context, _context); +} + + +// Declared in cformtst.h +void checkFormattedValue( + const char* message, + const UFormattedValue* fv, + const UChar* expectedString, + UFieldCategory expectedCategory, + const UFieldPosition* expectedFieldPositions, + int32_t expectedFieldPositionsLength) { + UErrorCode status = U_ZERO_ERROR; + int32_t length; + const UChar* actualString = ufmtval_getString(fv, &length, &status); + assertSuccess(message, &status); + // The string is guaranteed to be NUL-terminated. + int32_t actualLength = u_strlen(actualString); + assertIntEquals(message, actualLength, length); + assertUEquals(message, expectedString, actualString); +} + + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/cintltst/unumberformattertst.c b/icu4c/source/test/cintltst/unumberformattertst.c index 265fc47bc42..ff2fa7af22a 100644 --- a/icu4c/source/test/cintltst/unumberformattertst.c +++ b/icu4c/source/test/cintltst/unumberformattertst.c @@ -12,6 +12,7 @@ #include "unicode/unumberformatter.h" #include "unicode/umisc.h" #include "unicode/unum.h" +#include "cformtst.h" #include "cintltst.h" #include "cmemory.h" @@ -21,12 +22,17 @@ static void TestSkeletonFormatToFields(void); static void TestExampleCode(void); +static void TestFormattedValue(void); + void addUNumberFormatterTest(TestNode** root); +#define TESTCASE(x) addTest(root, &x, "tsformat/unumberformatter/" #x) + void addUNumberFormatterTest(TestNode** root) { - addTest(root, &TestSkeletonFormatToString, "unumberformatter/TestSkeletonFormatToString"); - addTest(root, &TestSkeletonFormatToFields, "unumberformatter/TestSkeletonFormatToFields"); - addTest(root, &TestExampleCode, "unumberformatter/TestExampleCode"); + TESTCASE(TestSkeletonFormatToString); + TESTCASE(TestSkeletonFormatToFields); + TESTCASE(TestExampleCode); + TESTCASE(TestFormattedValue); } @@ -188,4 +194,39 @@ static void TestExampleCode() { } +static void TestFormattedValue() { + UErrorCode ec = U_ZERO_ERROR; + UNumberFormatter* uformatter = unumf_openForSkeletonAndLocale( + u".00 compact-short", -1, "en", &ec); + assertSuccessCheck("Should create without error", &ec, TRUE); + UFormattedNumber* uresult = unumf_openResult(&ec); + assertSuccess("Should create result without error", &ec); + + unumf_formatInt(uformatter, 55000, uresult, &ec); // "55.00 K" + if (assertSuccessCheck("Should format without error", &ec, TRUE)) { + const UFormattedValue* fv = unumf_resultAsFormattedValue(uresult, &ec); + assertSuccess("Should convert without error", &ec); + static const UFieldPosition expectedFieldPositions[] = { + // field, begin index, end index + {UNUM_GROUPING_SEPARATOR_FIELD, 2, 3}, + {UNUM_GROUPING_SEPARATOR_FIELD, 6, 7}, + {UNUM_INTEGER_FIELD, 0, 10}, + {UNUM_GROUPING_SEPARATOR_FIELD, 13, 14}, + {UNUM_GROUPING_SEPARATOR_FIELD, 17, 18}, + {UNUM_INTEGER_FIELD, 11, 21}}; + checkFormattedValue( + "FormattedNumber as FormattedValue", + fv, + u"55.00K", + UFIELD_CATEGORY_NUMBER, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + + // cleanup: + unumf_closeResult(uresult); + unumf_close(uformatter); +} + + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index 00cc5ff2975..7bdcf66a509 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -935,6 +935,7 @@ group: number_representation resourcebundle int_functions ucase uniset_core + formatted_value group: numberformatter # ICU 60+ NumberFormatter API @@ -1032,6 +1033,11 @@ group: formattable_cnv deps formattable unistr_cnv conversion +group: formatted_value + formattedvalue.o + deps + platform + group: format format.o fphdlimp.o fpositer.o ufieldpositer.o deps diff --git a/icu4c/source/test/intltest/Makefile.in b/icu4c/source/test/intltest/Makefile.in index ad47ad0a14b..c049a5c691d 100644 --- a/icu4c/source/test/intltest/Makefile.in +++ b/icu4c/source/test/intltest/Makefile.in @@ -66,7 +66,8 @@ 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 numbertest_range.o erarulestest.o +static_unisets_test.o numfmtdatadriventest.o numbertest_range.o erarulestest.o \ +formattedvaluetest.o DEPS = $(OBJECTS:.o=.d) diff --git a/icu4c/source/test/intltest/formattedvaluetest.cpp b/icu4c/source/test/intltest/formattedvaluetest.cpp new file mode 100644 index 00000000000..1cd1e3c336d --- /dev/null +++ b/icu4c/source/test/intltest/formattedvaluetest.cpp @@ -0,0 +1,227 @@ +// © 2016 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 + +#include "unicode/formattedvalue.h" +#include "unicode/unum.h" +#include "intltest.h" +#include "itformat.h" + + +class FormattedValueTest : public IntlTest { +public: + void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=0); +private: + void testBasic(); + void testSetters(); + void testLocalPointer(); + + void assertAllPartsEqual( + UnicodeString messagePrefix, + const ConstrainedFieldPosition& cfpos, + UCFPosConstraintType constraint, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + int64_t context); +}; + +void FormattedValueTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char *) { + if (exec) { + logln("TestSuite FormattedValueTest: "); + } + TESTCASE_AUTO_BEGIN; + TESTCASE_AUTO(testBasic); + TESTCASE_AUTO(testSetters); + TESTCASE_AUTO(testLocalPointer); + TESTCASE_AUTO_END; +} + + +void FormattedValueTest::testBasic() { + IcuTestErrorCode status(*this, "testBasic"); + ConstrainedFieldPosition cfpos; + assertAllPartsEqual( + u"basic", + cfpos, + UCFPOS_CONSTRAINT_NONE, + UFIELD_CATEGORY_UNDEFINED, + 0, + 0, + 0, + 0LL); +} + +void FormattedValueTest::testSetters() { + IcuTestErrorCode status(*this, "testSetters"); + ConstrainedFieldPosition cfpos; + + cfpos.constrainCategory(UFIELD_CATEGORY_DATE); + assertAllPartsEqual( + u"setters 0", + cfpos, + UCFPOS_CONSTRAINT_CATEGORY, + UFIELD_CATEGORY_DATE, + 0, + 0, + 0, + 0LL); + + cfpos.constrainField(UFIELD_CATEGORY_NUMBER, UNUM_COMPACT_FIELD); + assertAllPartsEqual( + u"setters 1", + cfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 0, + 0, + 0LL); + + cfpos.setInt64IterationContext(42424242424242LL); + assertAllPartsEqual( + u"setters 2", + cfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 0, + 0, + 42424242424242LL); + + cfpos.setState(UFIELD_CATEGORY_NUMBER, UNUM_COMPACT_FIELD, 5, 10); + assertAllPartsEqual( + u"setters 3", + cfpos, + UCFPOS_CONSTRAINT_FIELD, + UFIELD_CATEGORY_NUMBER, + UNUM_COMPACT_FIELD, + 5, + 10, + 42424242424242LL); + + cfpos.reset(); + assertAllPartsEqual( + u"setters 4", + cfpos, + UCFPOS_CONSTRAINT_NONE, + UFIELD_CATEGORY_UNDEFINED, + 0, + 0, + 0, + 0LL); +} + +void FormattedValueTest::testLocalPointer() { + UErrorCode status = U_ZERO_ERROR; + LocalUConstrainedFieldPositionPointer ucfpos(ucfpos_open(&status)); + assertSuccess("Openining LocalUConstrainedFieldPositionPointer", status); + assertEquals(u"Test that object is valid", + UCFPOS_CONSTRAINT_NONE, + ucfpos_getConstraintType(ucfpos.getAlias(), &status)); + assertSuccess("Using LocalUConstrainedFieldPositionPointer", status); +} + +void FormattedValueTest::assertAllPartsEqual( + UnicodeString messagePrefix, + const ConstrainedFieldPosition& cfpos, + UCFPosConstraintType constraint, + UFieldCategory category, + int32_t field, + int32_t start, + int32_t limit, + int64_t context) { + assertEquals(messagePrefix + u": constraint", + constraint, cfpos.getConstraintType()); + assertEquals(messagePrefix + u": category", + category, cfpos.getCategory()); + assertEquals(messagePrefix + u": field", + field, cfpos.getField()); + assertEquals(messagePrefix + u": start", + start, cfpos.getStart()); + assertEquals(messagePrefix + u": limit", + limit, cfpos.getLimit()); + assertEquals(messagePrefix + u": context", + context, cfpos.getInt64IterationContext()); +} + + +void IntlTestWithFieldPosition::checkFormattedValue( + const char16_t* message, + const FormattedValue& fv, + UnicodeString expectedString, + UFieldCategory expectedCategory, + const UFieldPosition* expectedFieldPositions, + int32_t length) { + IcuTestErrorCode status(*this, "checkFormattedValue"); + UnicodeString baseMessage = UnicodeString(message) + u": " + fv.toString(status) + u": "; + + // Check string values + assertEquals(baseMessage + u"string", expectedString, fv.toString(status)); + assertEquals(baseMessage + u"temp string", expectedString, fv.toTempString(status)); + + // The temp string is guaranteed to be NUL-terminated + UnicodeString readOnlyAlias = fv.toTempString(status); + assertEquals(baseMessage + u"NUL-terminated", + 0, readOnlyAlias.getBuffer()[readOnlyAlias.length()]); + + // Check nextPosition over all fields + ConstrainedFieldPosition cfpos; + cfpos.constrainCategory(expectedCategory); + for (int32_t i = 0; i < length; i++) { + assertTrue(baseMessage + i, fv.nextPosition(cfpos, status)); + int32_t expectedField = expectedFieldPositions[i].field; + int32_t expectedStart = expectedFieldPositions[i].beginIndex; + int32_t expectedLimit = expectedFieldPositions[i].endIndex; + assertEquals(baseMessage + u"category " + Int64ToUnicodeString(i), + expectedCategory, cfpos.getCategory()); + assertEquals(baseMessage + u"field " + Int64ToUnicodeString(i), + expectedField, cfpos.getField()); + assertEquals(baseMessage + u"start " + Int64ToUnicodeString(i), + expectedStart, cfpos.getStart()); + assertEquals(baseMessage + u"limit " + Int64ToUnicodeString(i), + expectedLimit, cfpos.getLimit()); + } + assertFalse(baseMessage + u"after loop", fv.nextPosition(cfpos, status)); + + // Check nextPosition constrained over each field one at a time + std::set uniqueFields; + for (int32_t i = 0; i < length; i++) { + uniqueFields.insert(expectedFieldPositions[i].field); + } + for (int32_t field : uniqueFields) { + cfpos.reset(); + cfpos.constrainField(expectedCategory, field); + for (int32_t i = 0; i < length; i++) { + if (expectedFieldPositions[i].field != field) { + continue; + } + assertTrue(baseMessage + i, fv.nextPosition(cfpos, status)); + int32_t expectedField = expectedFieldPositions[i].field; + int32_t expectedStart = expectedFieldPositions[i].beginIndex; + int32_t expectedLimit = expectedFieldPositions[i].endIndex; + assertEquals(baseMessage + u"category " + Int64ToUnicodeString(i), + expectedCategory, cfpos.getCategory()); + assertEquals(baseMessage + u"field " + Int64ToUnicodeString(i), + expectedField, cfpos.getField()); + assertEquals(baseMessage + u"start " + Int64ToUnicodeString(i), + expectedStart, cfpos.getStart()); + assertEquals(baseMessage + u"limit " + Int64ToUnicodeString(i), + expectedLimit, cfpos.getLimit()); + } + assertFalse(baseMessage + u"after loop", fv.nextPosition(cfpos, status)); + } +} + + +extern IntlTest *createFormattedValueTest() { + return new FormattedValueTest(); +} + +#endif /* !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/intltest.vcxproj b/icu4c/source/test/intltest/intltest.vcxproj index df00ebb8829..298a7f5c07d 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj +++ b/icu4c/source/test/intltest/intltest.vcxproj @@ -363,6 +363,7 @@ + diff --git a/icu4c/source/test/intltest/intltest.vcxproj.filters b/icu4c/source/test/intltest/intltest.vcxproj.filters index f1b740dbe99..d707727e34c 100644 --- a/icu4c/source/test/intltest/intltest.vcxproj.filters +++ b/icu4c/source/test/intltest/intltest.vcxproj.filters @@ -538,6 +538,9 @@ formatting + + formatting + diff --git a/icu4c/source/test/intltest/itformat.cpp b/icu4c/source/test/intltest/itformat.cpp index d450922eb6e..1c993fc2a87 100644 --- a/icu4c/source/test/intltest/itformat.cpp +++ b/icu4c/source/test/intltest/itformat.cpp @@ -71,6 +71,7 @@ extern IntlTest *createTimeUnitTest(); extern IntlTest *createMeasureFormatTest(); extern IntlTest *createNumberFormatSpecificationTest(); extern IntlTest *createScientificNumberFormatterTest(); +extern IntlTest *createFormattedValueTest(); #define TESTCLASS(id, TestClass) \ @@ -217,6 +218,15 @@ void IntlTestFormat::runIndexedTest( int32_t index, UBool exec, const char* &nam TESTCLASS(50,NumberFormatDataDrivenTest); TESTCLASS(51,NumberTest); TESTCLASS(52,EraRulesTest); + case 53: + name = "FormattedValueTest"; + if (exec) { + logln("FormattedValueTest test---"); + logln((UnicodeString)""); + LocalPointer test(createFormattedValueTest()); + callTest(*test, par); + } + break; default: name = ""; break; //needed to end loop } if (exec) { diff --git a/icu4c/source/test/intltest/itformat.h b/icu4c/source/test/intltest/itformat.h index 5af7601fb56..8069b6d9986 100644 --- a/icu4c/source/test/intltest/itformat.h +++ b/icu4c/source/test/intltest/itformat.h @@ -17,6 +17,7 @@ #if !UCONFIG_NO_FORMATTING +#include "unicode/formattedvalue.h" #include "intltest.h" @@ -24,6 +25,20 @@ class IntlTestFormat: public IntlTest { void runIndexedTest( int32_t index, UBool exec, const char* &name, char* par = NULL ); }; + +class IntlTestWithFieldPosition : public IntlTest { +public: + // TODO: When needed, add overload with a different category for each position + void checkFormattedValue( + const char16_t* message, + const FormattedValue& fv, + UnicodeString expectedString, + UFieldCategory expectedCategory, + const UFieldPosition* expectedFieldPositions, + int32_t length); +}; + + #endif /* #if !UCONFIG_NO_FORMATTING */ #endif diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 66ca41e2f5c..5750cecde69 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -8,6 +8,7 @@ #include "number_stringbuilder.h" #include "intltest.h" +#include "itformat.h" #include "number_affixutils.h" #include "numparse_stringsegment.h" #include "numrange_impl.h" @@ -43,7 +44,7 @@ class AffixUtilsTest : public IntlTest { UErrorCode &status); }; -class NumberFormatterApiTest : public IntlTest { +class NumberFormatterApiTest : public IntlTestWithFieldPosition { public: NumberFormatterApiTest(); NumberFormatterApiTest(UErrorCode &status); @@ -119,8 +120,11 @@ class NumberFormatterApiTest : public IntlTest { void assertUndefinedSkeleton(const UnlocalizedNumberFormatter& f); - void assertFieldPositions(const char16_t* message, const FormattedNumber& formattedNumber, - const UFieldPosition* expectedFieldPositions, int32_t length); + void assertNumberFieldPositions( + const char16_t* message, + const FormattedNumber& formattedNumber, + const UFieldPosition* expectedFieldPositions, + int32_t length); }; class DecimalQuantityTest : public IntlTest { @@ -253,7 +257,7 @@ class NumberSkeletonTest : public IntlTest { void expectedErrorSkeleton(const char16_t** cases, int32_t casesLen); }; -class NumberRangeFormatterTest : public IntlTest { +class NumberRangeFormatterTest : public IntlTestWithFieldPosition { public: NumberRangeFormatterTest(); NumberRangeFormatterTest(UErrorCode &status); @@ -264,6 +268,7 @@ class NumberRangeFormatterTest : public IntlTest { void testIdentity(); void testDifferentFormatters(); void testPlurals(); + void testFieldPositions(); void testCopyMove(); void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0); @@ -293,7 +298,7 @@ class NumberRangeFormatterTest : public IntlTest { const char16_t* expected_50K_50K, const char16_t* expected_50K_50M); - void assertFormattedRangeEquals( + FormattedNumberRange assertFormattedRangeEquals( const char16_t* message, const LocalizedNumberRangeFormatter& l, double first, diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 0ae6d83a057..52e0bee24d1 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -2188,11 +2188,11 @@ void NumberFormatterApiTest::fieldPositionLogic() { {UNUM_DECIMAL_SEPARATOR_FIELD, 14, 15}, {UNUM_FRACTION_FIELD, 15, 17}}; - assertFieldPositions( + assertNumberFieldPositions( message, fmtd, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); // Test the iteration functionality of nextFieldPosition FieldPosition actual = {UNUM_GROUPING_SEPARATOR_FIELD}; @@ -2236,11 +2236,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 0, 2}, {UNUM_MEASURE_UNIT_FIELD, 2, 4}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2257,11 +2257,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_INTEGER_FIELD, 0, 2}, // coverage for old enum: {DecimalFormat::kMeasureUnitField, 2, 6}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2278,11 +2278,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_INTEGER_FIELD, 0, 2}, // note: field starts after the space {UNUM_MEASURE_UNIT_FIELD, 3, 9}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2299,11 +2299,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_MEASURE_UNIT_FIELD, 0, 11}, {UNUM_INTEGER_FIELD, 12, 14}, {UNUM_MEASURE_UNIT_FIELD, 15, 19}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2320,11 +2320,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_INTEGER_FIELD, 0, 2}, // Should trim leading/trailing spaces, but not inner spaces: {UNUM_MEASURE_UNIT_FIELD, 3, 7}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2343,11 +2343,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 1, 3}, {UNUM_MEASURE_UNIT_FIELD, 4, 5}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2363,11 +2363,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 0, 2}, {UNUM_COMPACT_FIELD, 2, 3}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2383,11 +2383,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 0, 2}, {UNUM_COMPACT_FIELD, 3, 11}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2403,11 +2403,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 0, 1}, {UNUM_COMPACT_FIELD, 2, 9}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2423,11 +2423,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { // field, begin index, end index {UNUM_INTEGER_FIELD, 1, 2}, {UNUM_COMPACT_FIELD, 3, 6}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2444,11 +2444,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_INTEGER_FIELD, 0, 2}, {UNUM_COMPACT_FIELD, 3, 8}, {UNUM_CURRENCY_FIELD, 9, 12}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } { @@ -2467,11 +2467,11 @@ void NumberFormatterApiTest::fieldPositionCoverage() { {UNUM_INTEGER_FIELD, 0, 2}, {UNUM_COMPACT_FIELD, 3, 11}, {UNUM_MEASURE_UNIT_FIELD, 12, 18}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions, - sizeof(expectedFieldPositions)/sizeof(*expectedFieldPositions)); + UPRV_LENGTHOF(expectedFieldPositions)); } } @@ -2882,10 +2882,30 @@ void NumberFormatterApiTest::assertUndefinedSkeleton(const UnlocalizedNumberForm status); } -void NumberFormatterApiTest::assertFieldPositions( +void NumberFormatterApiTest::assertNumberFieldPositions( const char16_t* message, const FormattedNumber& formattedNumber, const UFieldPosition* expectedFieldPositions, int32_t length) { - IcuTestErrorCode status(*this, "assertFieldPositions"); + IcuTestErrorCode status(*this, "assertNumberFieldPositions"); + + // Check FormattedValue functions + checkFormattedValue( + message, + static_cast(formattedNumber), + formattedNumber.toString(status), + UFIELD_CATEGORY_NUMBER, + expectedFieldPositions, + length); + + // Check no field positions in an unrelated category + checkFormattedValue( + message, + static_cast(formattedNumber), + formattedNumber.toString(status), + UFIELD_CATEGORY_DATE, + nullptr, + 0); + + // Check FormattedNumber-specific functions UnicodeString baseMessage = UnicodeString(message) + u": " + formattedNumber.toString(status) + u": "; FieldPositionIterator fpi; formattedNumber.getAllFieldPositions(fpi, status); diff --git a/icu4c/source/test/intltest/numbertest_range.cpp b/icu4c/source/test/intltest/numbertest_range.cpp index 571864644f6..5012a7ad49b 100644 --- a/icu4c/source/test/intltest/numbertest_range.cpp +++ b/icu4c/source/test/intltest/numbertest_range.cpp @@ -48,6 +48,7 @@ void NumberRangeFormatterTest::runIndexedTest(int32_t index, UBool exec, const c TESTCASE_AUTO(testIdentity); TESTCASE_AUTO(testDifferentFormatters); TESTCASE_AUTO(testPlurals); + TESTCASE_AUTO(testFieldPositions); TESTCASE_AUTO(testCopyMove); TESTCASE_AUTO_END; } @@ -723,6 +724,63 @@ void NumberRangeFormatterTest::testPlurals() { } } +void NumberRangeFormatterTest::testFieldPositions() { + { + const char16_t* message = u"Field position test 1"; + const char16_t* expectedString = u"3K – 5K m"; + FormattedNumberRange result = assertFormattedRangeEquals( + message, + NumberRangeFormatter::with() + .numberFormatterBoth(NumberFormatter::with() + .unit(METER) + .notation(Notation::compactShort())) + .locale("en-us"), + 3000, + 5000, + expectedString); + static const UFieldPosition expectedFieldPositions[] = { + // field, begin index, end index + {UNUM_INTEGER_FIELD, 0, 1}, + {UNUM_COMPACT_FIELD, 1, 2}, + {UNUM_INTEGER_FIELD, 5, 6}, + {UNUM_COMPACT_FIELD, 6, 7}, + {UNUM_MEASURE_UNIT_FIELD, 8, 9}}; + checkFormattedValue( + message, + result, + expectedString, + UFIELD_CATEGORY_NUMBER, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + + { + const char16_t* message = u"Field position test 2"; + const char16_t* expectedString = u"87,654,321–98,765,432"; + FormattedNumberRange result = assertFormattedRangeEquals( + message, + NumberRangeFormatter::withLocale("en-us"), + 87654321, + 98765432, + expectedString); + static const UFieldPosition expectedFieldPositions[] = { + // field, begin index, end index + {UNUM_GROUPING_SEPARATOR_FIELD, 2, 3}, + {UNUM_GROUPING_SEPARATOR_FIELD, 6, 7}, + {UNUM_INTEGER_FIELD, 0, 10}, + {UNUM_GROUPING_SEPARATOR_FIELD, 13, 14}, + {UNUM_GROUPING_SEPARATOR_FIELD, 17, 18}, + {UNUM_INTEGER_FIELD, 11, 21}}; + checkFormattedValue( + message, + result, + expectedString, + UFIELD_CATEGORY_NUMBER, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } +} + void NumberRangeFormatterTest::testCopyMove() { IcuTestErrorCode status(*this, "testCopyMove"); @@ -792,7 +850,7 @@ void NumberRangeFormatterTest::assertFormatRange( assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M); } -void NumberRangeFormatterTest::assertFormattedRangeEquals( +FormattedNumberRange NumberRangeFormatterTest::assertFormattedRangeEquals( const char16_t* message, const LocalizedNumberRangeFormatter& l, double first, @@ -801,8 +859,10 @@ void NumberRangeFormatterTest::assertFormattedRangeEquals( 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); + FormattedNumberRange fnr = l.formatFormattableRange(first, second, status); + UnicodeString actual = fnr.toString(status); assertEquals(fullMessage, expected, actual); + return fnr; } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java index 7d349d1f22c..5102db4b518 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java @@ -10,6 +10,8 @@ import java.util.HashMap; import java.util.Map; import com.ibm.icu.impl.StaticUnicodeSets; +import com.ibm.icu.text.ConstrainedFieldPosition; +import com.ibm.icu.text.ConstrainedFieldPosition.ConstraintType; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.NumberFormat.Field; import com.ibm.icu.text.UnicodeSet; @@ -364,17 +366,27 @@ public class NumberStringBuilder implements CharSequence { return chars.length; } + /** Note: this returns a NumberStringBuilder. Do not return publicly. */ @Override + @Deprecated public CharSequence subSequence(int start, int end) { - if (start < 0 || end > length || end < start) { - throw new IndexOutOfBoundsException(); - } + assert start >= 0; + assert end <= length; + assert end >= start; NumberStringBuilder other = new NumberStringBuilder(this); other.zero = zero + start; other.length = end - start; return other; } + /** Use this instead of subSequence if returning publicly. */ + public String subString(int start, int end) { + if (start < 0 || end > length || end < start) { + throw new IndexOutOfBoundsException(); + } + return new String(chars, start + zero, end - start); + } + /** * Returns the string represented by the characters in this string builder. * @@ -400,6 +412,8 @@ public class NumberStringBuilder implements CharSequence { fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%'); fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰'); fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$'); + fieldToDebugChar.put(NumberFormat.Field.MEASURE_UNIT, 'u'); + fieldToDebugChar.put(NumberFormat.Field.COMPACT, 'C'); } /** @@ -483,13 +497,6 @@ public class NumberStringBuilder implements CharSequence { throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); } - /** - * Populates the given {@link FieldPosition} based on this string builder. - * - * @param fp - * The FieldPosition to populate. - * @return true if the field was found; false if it was not found. - */ public boolean nextFieldPosition(FieldPosition fp) { java.text.Format.Field rawField = fp.getFieldAttribute(); @@ -511,99 +518,120 @@ public class NumberStringBuilder implements CharSequence { + rawField.getClass().toString()); } - NumberFormat.Field field = (NumberFormat.Field) rawField; + ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); + cfpos.constrainField(rawField); + cfpos.setState(rawField, null, fp.getBeginIndex(), fp.getEndIndex()); + if (nextPosition(cfpos)) { + fp.setBeginIndex(cfpos.getStart()); + fp.setEndIndex(cfpos.getLimit()); + return true; + } - boolean seenStart = false; - int fractionStart = -1; - int startIndex = fp.getEndIndex(); - for (int i = zero + startIndex; i <= zero + length; i++) { - Field _field = (i < zero + length) ? fields[i] : null; - if (seenStart && field != _field) { - // Special case: GROUPING_SEPARATOR counts as an INTEGER. - if (field == NumberFormat.Field.INTEGER - && _field == NumberFormat.Field.GROUPING_SEPARATOR) { - continue; - } - fp.setEndIndex(i - zero); - // Trim ignorables (whitespace, etc.) from the edge of the field. - if (trimFieldPosition(fp)) { + // Special case: fraction should start after integer if fraction is not present + if (rawField == NumberFormat.Field.FRACTION && fp.getEndIndex() == 0) { + boolean inside = false; + int i = zero; + for (; i < zero + length; i++) { + if (isIntOrGroup(fields[i]) || fields[i] == NumberFormat.Field.DECIMAL_SEPARATOR) { + inside = true; + } else if (inside) { break; } - // This position was all ignorables; continue to the next position. - seenStart = false; - } else if (!seenStart && field == _field) { - fp.setBeginIndex(i - zero); - seenStart = true; - } - if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) { - fractionStart = i - zero + 1; } + fp.setBeginIndex(i - zero); + fp.setEndIndex(i - zero); } - // Backwards compatibility: FRACTION needs to start after INTEGER if empty. - // Do not return that a field was found, though, since there is not actually a fraction part. - if (field == NumberFormat.Field.FRACTION && !seenStart && fractionStart != -1) { - fp.setBeginIndex(fractionStart); - fp.setEndIndex(fractionStart); - } - - return seenStart; + return false; } public AttributedCharacterIterator toCharacterIterator() { + ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); AttributedString as = new AttributedString(toString()); - Field current = null; - int currentStart = -1; - for (int i = 0; i < length; i++) { - Field field = fields[i + zero]; - if (current == NumberFormat.Field.INTEGER - && field == NumberFormat.Field.GROUPING_SEPARATOR) { - // Special case: GROUPING_SEPARATOR counts as an INTEGER. - // TODO(ICU-13064): Grouping separator can be more than 1 code unit. - as.addAttribute(NumberFormat.Field.GROUPING_SEPARATOR, - NumberFormat.Field.GROUPING_SEPARATOR, - i, - i + 1); - } else if (current != field) { - if (current != null) { - FieldPosition fp = new FieldPosition(null); - fp.setBeginIndex(currentStart); - fp.setEndIndex(i); - if (trimFieldPosition(fp)) { - as.addAttribute(current, current, fp.getBeginIndex(), fp.getEndIndex()); - } - } - current = field; - currentStart = i; - } + while (this.nextPosition(cfpos)) { + // Backwards compatibility: field value = field + as.addAttribute(cfpos.getField(), cfpos.getField(), cfpos.getStart(), cfpos.getLimit()); } - if (current != null) { - FieldPosition fp = new FieldPosition(null); - fp.setBeginIndex(currentStart); - fp.setEndIndex(length); - if (trimFieldPosition(fp)) { - as.addAttribute(current, current, fp.getBeginIndex(), fp.getEndIndex()); - } - } - return as.getIterator(); } - private boolean trimFieldPosition(FieldPosition fp) { - // Trim ignorables from the back - int endIgnorablesIndex = StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) - .spanBack(this, fp.getEndIndex(), UnicodeSet.SpanCondition.CONTAINED); - - // Check if the entire segment is ignorables - if (endIgnorablesIndex <= fp.getBeginIndex()) { + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + if (cfpos.getConstraintType() == ConstraintType.CLASS + && !cfpos.getClassConstraint().isAssignableFrom(NumberFormat.Field.class)) { return false; } - fp.setEndIndex(endIgnorablesIndex); - // Trim ignorables from the front - int startIgnorablesIndex = StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) - .span(this, fp.getBeginIndex(), UnicodeSet.SpanCondition.CONTAINED); - fp.setBeginIndex(startIgnorablesIndex); - return true; + boolean isSearchingForField = (cfpos.getConstraintType() == ConstraintType.FIELD); + + int fieldStart = -1; + Field currField = null; + for (int i = zero + cfpos.getLimit(); i <= zero + length; i++) { + Field _field = (i < zero + length) ? fields[i] : null; + // Case 1: currently scanning a field. + if (currField != null) { + if (currField != _field) { + int end = i - zero; + // Grouping separators can be whitespace; don't throw them out! + if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + end = trimBack(end); + } + if (end <= fieldStart) { + // Entire field position is ignorable; skip. + fieldStart = -1; + currField = null; + i--; // look at this index again + continue; + } + int start = fieldStart; + if (currField != NumberFormat.Field.GROUPING_SEPARATOR) { + start = trimFront(start); + } + cfpos.setState(currField, null, start, end); + return true; + } + continue; + } + // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER. + if ((!isSearchingForField || cfpos.getField() == NumberFormat.Field.INTEGER) + && i > zero + && i - zero > cfpos.getLimit() // don't return the same field twice in a row + && isIntOrGroup(fields[i - 1]) + && !isIntOrGroup(_field)) { + int j = i - 1; + for (; j >= zero && isIntOrGroup(fields[j]); j--) {} + cfpos.setState(NumberFormat.Field.INTEGER, null, j - zero + 1, i - zero); + return true; + } + // Special case: skip over INTEGER; will be coalesced later. + if (_field == NumberFormat.Field.INTEGER) { + _field = null; + } + // Case 2: no field starting at this position. + if (_field == null) { + continue; + } + // Case 3: check for field starting at this position + if (!isSearchingForField || cfpos.getField() == _field) { + fieldStart = i - zero; + currField = _field; + } + } + + assert currField == null; + return false; + } + + private static boolean isIntOrGroup(NumberFormat.Field field) { + return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR; + } + + private int trimBack(int limit) { + return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) + .spanBack(this, limit, UnicodeSet.SpanCondition.CONTAINED); + } + + private int trimFront(int start) { + return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) + .span(this, start, UnicodeSet.SpanCondition.CONTAINED); } } 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 9febde4358c..615b79e5fc5 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 @@ -10,6 +10,8 @@ import java.util.Arrays; import com.ibm.icu.impl.number.DecimalQuantity; import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.text.ConstrainedFieldPosition; +import com.ibm.icu.text.FormattedValue; import com.ibm.icu.text.PluralRules.IFixedDecimal; import com.ibm.icu.util.ICUUncheckedIOException; @@ -21,7 +23,7 @@ import com.ibm.icu.util.ICUUncheckedIOException; * @provisional This API might change or be removed in a future release. * @see NumberFormatter */ -public class FormattedNumber { +public class FormattedNumber implements FormattedValue { final NumberStringBuilder nsb; final DecimalQuantity fq; @@ -31,12 +33,10 @@ public class FormattedNumber { } /** - * Creates a String representation of the the formatted number. + * {@inheritDoc} * - * @return a String containing the localized number. * @draft ICU 60 * @provisional This API might change or be removed in a future release. - * @see NumberFormatter */ @Override public String toString() { @@ -44,21 +44,12 @@ public class FormattedNumber { } /** - * Append the formatted number to an Appendable, such as a StringBuilder. This may be slightly more - * efficient than creating a String. + * {@inheritDoc} * - *

- * 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 string. * @return The same Appendable, for chaining. * @draft ICU 60 - * @provisional This API might change or be removed in a future release. - * @see Appendable - * @see NumberFormatter */ + @Override public A appendTo(A appendable) { try { appendable.append(nsb); @@ -69,6 +60,50 @@ public class FormattedNumber { return appendable; } + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public char charAt(int index) { + return nsb.charAt(index); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int length() { + return nsb.length(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public CharSequence subSequence(int start, int end) { + return nsb.subString(start, end); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + return nsb.nextPosition(cfpos); + } + /** * 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, @@ -155,20 +190,12 @@ public class FormattedNumber { } /** - * Export the formatted number 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. + * {@inheritDoc} * - * @return An AttributedCharacterIterator, containing information on the field attributes of the - * number string. * @draft ICU 62 * @provisional This API might change or be removed in a future release. - * @see com.ibm.icu.text.NumberFormat.Field - * @see AttributedCharacterIterator - * @see NumberFormatter */ + @Override public AttributedCharacterIterator toCharacterIterator() { return nsb.toCharacterIterator(); } 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 index ad1ab6b06ea..56f2481fae6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FormattedNumberRange.java @@ -11,6 +11,8 @@ 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.text.ConstrainedFieldPosition; +import com.ibm.icu.text.FormattedValue; import com.ibm.icu.util.ICUUncheckedIOException; /** @@ -22,7 +24,7 @@ import com.ibm.icu.util.ICUUncheckedIOException; * @provisional This API might change or be removed in a future release. * @see NumberRangeFormatter */ -public class FormattedNumberRange { +public class FormattedNumberRange implements FormattedValue { final NumberStringBuilder string; final DecimalQuantity quantity1; final DecimalQuantity quantity2; @@ -37,12 +39,10 @@ public class FormattedNumberRange { } /** - * Creates a String representation of the the formatted number range. + * {@inheritDoc} * - * @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() { @@ -50,21 +50,12 @@ public class FormattedNumberRange { } /** - * Append the formatted number range to an Appendable, such as a StringBuilder. This may be slightly more efficient - * than creating a String. + * {@inheritDoc} * - *

- * 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 */ + @Override public A appendTo(A appendable) { try { appendable.append(string); @@ -75,6 +66,50 @@ public class FormattedNumberRange { return appendable; } + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public char charAt(int index) { + return string.charAt(index); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public int length() { + return string.length(); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public CharSequence subSequence(int start, int end) { + return string.subString(start, end); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public boolean nextPosition(ConstrainedFieldPosition cfpos) { + return string.nextPosition(cfpos); + } + /** * 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, @@ -109,20 +144,12 @@ public class FormattedNumberRange { } /** - * 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. + * {@inheritDoc} * - * @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 */ + @Override public AttributedCharacterIterator toCharacterIterator() { return string.toCharacterIterator(); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/ConstrainedFieldPosition.java b/icu4j/main/classes/core/src/com/ibm/icu/text/ConstrainedFieldPosition.java new file mode 100644 index 00000000000..7a44f93ea91 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/ConstrainedFieldPosition.java @@ -0,0 +1,320 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.text; + +import java.text.Format.Field; + +/** + * Represents a span of a string containing a given field. + * + * This class differs from FieldPosition in the following ways: + * + * 1. It has information on the field category. + * 2. It allows you to set constraints to use when iterating over field positions. + * 3. It is used for the newer FormattedValue APIs. + * + * @author sffc + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ +public class ConstrainedFieldPosition { + + /** + * Represents the type of constraint for ConstrainedFieldPosition. + * + * Constraints are used to control the behavior of iteration in FormattedValue. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public enum ConstraintType { + /** + * Represents the lack of a constraint. + * + * This is the return value of {@link #getConstraintType} + * if no "constrain" methods were called. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + NONE, + + /** + * Represents that the field class is constrained. + * Use {@link #getClassConstraint} to access the class. + * + * This is the return value of @link #getConstraintType} + * after {@link #constrainClass} is called. + * + * FormattedValue implementations should not change the field when this constraint is active. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + CLASS, + + /** + * Represents that the field is constrained. + * Use {@link #getField} to access the field. + * + * This is the return value of @link #getConstraintType} + * after {@link #constrainField} is called. + * + * FormattedValue implementations should not change the field when this constraint is active. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + FIELD + }; + + private ConstraintType fConstraint; + private Class fClassConstraint; + private Field fField; + private Object fValue; + private int fStart; + private int fLimit; + private long fContext; + + /** + * Initializes a CategoryFieldPosition. + * + * By default, the CategoryFieldPosition has no iteration constraints. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public ConstrainedFieldPosition() { + reset(); + } + + /** + * Resets this ConstrainedFieldPosition to its initial state, as if it were newly created: + * + * - Removes any constraints that may have been set on the instance. + * - Resets the iteration position. + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public void reset() { + fConstraint = ConstraintType.NONE; + fClassConstraint = Object.class; + fField = null; + fValue = null; + fStart = 0; + fLimit = 0; + fContext = 0; + } + + /** + * Sets a constraint on the field. + * + * When this instance of ConstrainedFieldPosition is passed to {@link FormattedValue#nextPosition}, positions are + * skipped unless they have the given category and field. + * + * Any previously set constraints are cleared. + * + * For example, to loop over all grouping separators: + * + *

+     * ConstrainedFieldPosition cfpos;
+     * cfpos.constrainField(NumberFormat.Field.GROUPING_SEPARATOR);
+     * while (fmtval.nextPosition(cfpos)) {
+     *   // handle the grouping separator position
+     * }
+     * 
+ * + * Changing the constraint while in the middle of iterating over a FormattedValue + * does not generally have well-defined behavior. + * + * @param field + * The field to fix when iterating. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public void constrainField(Field field) { + if (field == null) { + throw new IllegalArgumentException("Cannot constrain on null field"); + } + fConstraint = ConstraintType.FIELD; + fClassConstraint = Object.class; + fField = field; + } + + /** + * Sets a constraint on the field class. + * + * When this instance of ConstrainedFieldPosition is passed to {@link FormattedValue#nextPosition}, positions are + * skipped unless the field is an instance of the class constraint, including subclasses. + * + * Any previously set constraints are cleared. + * + * For example, to loop over only the number-related fields: + * + *
+     * ConstrainedFieldPosition cfpos;
+     * cfpos.constrainClass(NumberFormat.Field.class);
+     * while (fmtval.nextPosition(cfpos)) {
+     *   // handle the number-related field position
+     * }
+     * 
+ * + * @param classConstraint + * The field class to fix when iterating. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public void constrainClass(Class classConstraint) { + if (classConstraint == null) { + throw new IllegalArgumentException("Cannot constrain on null field class"); + } + fConstraint = ConstraintType.CLASS; + fClassConstraint = classConstraint; + fField = null; + } + + /** + * Gets the currently active constraint. + * + * @return The currently active constraint type. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public ConstraintType getConstraintType() { + return fConstraint; + } + + /** + * Gets the class on which field positions are currently constrained. + * + * @return The class constraint from {@link #constrainClass}, or Object.class by default. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public Class getClassConstraint() { + return fClassConstraint; + } + + /** + * Gets the field for the current position. + * + * If a field constraint was set, this function returns the constrained + * field. Otherwise, the return value is well-defined and non-null only after + * FormattedValue#nextPosition returns TRUE. + * + * @return The field saved in the instance. See above for null conditions. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public Field getField() { + return fField; + } + + /** + * Gets the INCLUSIVE start index for the current position. + * + * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. + * + * @return The start index saved in the instance. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public int getStart() { + return fStart; + } + + /** + * Gets the EXCLUSIVE end index stored for the current position. + * + * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. + * + * @return The end index saved in the instance. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public int getLimit() { + return fLimit; + } + + /** + * Gets the value associated with the current field position. The field value is often not set. + * + * The return value is well-defined only after FormattedValue#nextPosition returns TRUE. + * + * @return The value for the current position. Might be null. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public Object getFieldValue() { + return fValue; + } + + /** + * Gets an int64 that FormattedValue implementations may use for storage. + * + * The initial value is zero. + * + * Users of FormattedValue should not need to call this method. + * + * @return The current iteration context from {@link #setInt64IterationContext}. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public long getInt64IterationContext() { + return fContext; + } + + /** + * Sets an int64 that FormattedValue implementations may use for storage. + * + * Intended to be used by FormattedValue implementations. + * + * @param context + * The new iteration context. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public void setInt64IterationContext(long context) { + fContext = context; + } + + /** + * Sets new values for the primary public getters. + * + * Intended to be used by FormattedValue implementations. + * + * It is up to the implementation to ensure that the user-requested + * constraints are satisfied. This method does not check! + * + * @param field + * The new field. + * @param value + * The new field value. + * @param start + * The new inclusive start index. + * @param limit + * The new exclusive end index. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public void setState(Field field, Object value, int start, int limit) { + fField = field; + fValue = value; + fStart = start; + fLimit = limit; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("CFPos["); + sb.append(fStart); + sb.append('-'); + sb.append(fLimit); + sb.append(' '); + sb.append(fField); + sb.append(']'); + return sb.toString(); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/FormattedValue.java b/icu4j/main/classes/core/src/com/ibm/icu/text/FormattedValue.java new file mode 100644 index 00000000000..841a3133659 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/FormattedValue.java @@ -0,0 +1,76 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.text; + +import java.text.AttributedCharacterIterator; + +import com.ibm.icu.util.ICUUncheckedIOException; + +/** + * An abstract formatted value: a string with associated field attributes. + * Many formatters format to classes implementing FormattedValue. + * + * @author sffc + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ +public interface FormattedValue extends CharSequence { + /** + * Returns the formatted string as a Java String. + * + * Consider using {@link #appendTo} for greater efficiency. + * + * @return The formatted string. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public String toString(); + + /** + * Appends the formatted string to an Appendable. + *

+ * 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 string output. + * @return The same Appendable, for chaining. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public A appendTo(A appendable); + + /** + * Iterates over field positions in the FormattedValue. This lets you determine the position + * of specific types of substrings, like a month or a decimal separator. + * + * To loop over all field positions: + * + *

+     *     ConstrainableFieldPosition cfpos = new ConstrainableFieldPosition();
+     *     while (fmtval.nextPosition(cfpos)) {
+     *         // handle the field position; get information from cfpos
+     *     }
+     * 
+ * + * @param cfpos + * The object used for iteration state. This can provide constraints to iterate over + * only one specific field; see {@link ConstrainedFieldPosition#constrainField}. + * @return true if a new occurrence of the field was found; + * false otherwise. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public boolean nextPosition(ConstrainedFieldPosition cfpos); + + /** + * Exports the formatted number as an AttributedCharacterIterator. + *

+ * Consider using {@link #nextPosition} if you are trying to get field information. + * + * @return An AttributedCharacterIterator containing full field information. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public AttributedCharacterIterator toCharacterIterator(); +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java new file mode 100644 index 00000000000..c1a037b364d --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java @@ -0,0 +1,243 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.format; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.text.AttributedCharacterIterator; +import java.text.Format; +import java.text.Format.Field; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.ibm.icu.text.ConstrainedFieldPosition; +import com.ibm.icu.text.ConstrainedFieldPosition.ConstraintType; +import com.ibm.icu.text.FormattedValue; +import com.ibm.icu.text.NumberFormat; + +/** + * @author sffc + */ +public class FormattedValueTest { + @Test + public void testBasic() { + ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); + assertAllPartsEqual( + "basic", + cfpos, + ConstraintType.NONE, + Object.class, + null, + null, + 0, + 0, + 0L); + } + + @Test + public void testSetters() { + ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); + + cfpos.constrainField(NumberFormat.Field.COMPACT); + assertAllPartsEqual( + "setters 1", + cfpos, + ConstraintType.FIELD, + Object.class, + NumberFormat.Field.COMPACT, + null, + 0, + 0, + 0L); + + cfpos.constrainClass(NumberFormat.Field.class); + assertAllPartsEqual( + "setters 1.5", + cfpos, + ConstraintType.CLASS, + NumberFormat.Field.class, + null, + null, + 0, + 0, + 0L); + + cfpos.setInt64IterationContext(42424242424242L); + assertAllPartsEqual( + "setters 2", + cfpos, + ConstraintType.CLASS, + NumberFormat.Field.class, + null, + null, + 0, + 0, + 42424242424242L); + + cfpos.setState(NumberFormat.Field.COMPACT, BigDecimal.ONE, 5, 10); + assertAllPartsEqual( + "setters 3", + cfpos, + ConstraintType.CLASS, + NumberFormat.Field.class, + NumberFormat.Field.COMPACT, + BigDecimal.ONE, + 5, + 10, + 42424242424242L); + + cfpos.reset(); + assertAllPartsEqual( + "setters 4", + cfpos, + ConstraintType.NONE, + Object.class, + null, + null, + 0, + 0, + 0L); + } + + private void assertAllPartsEqual(String messagePrefix, ConstrainedFieldPosition cfpos, ConstraintType constraint, + Class classConstraint, Field field, Object value, int start, int limit, long context) { + assertEquals(messagePrefix + ": constraint", constraint, cfpos.getConstraintType()); + assertEquals(messagePrefix + ": class constraint", classConstraint, cfpos.getClassConstraint()); + assertEquals(messagePrefix + ": field", field, cfpos.getField()); + assertEquals(messagePrefix + ": field value", value, cfpos.getFieldValue()); + assertEquals(messagePrefix + ": start", start, cfpos.getStart()); + assertEquals(messagePrefix + ": limit", limit, cfpos.getLimit()); + assertEquals(messagePrefix + ": context", context, cfpos.getInt64IterationContext()); + } + + public static void checkFormattedValue(String message, FormattedValue fv, String expectedString, + Object[][] expectedFieldPositions) { + // Calculate some initial expected values + int stringLength = fv.toString().length(); + HashSet uniqueFields = new HashSet<>(); + Set> uniqueFieldClasses = new HashSet<>(); + for (int i=0; i allAttributes = fpi.getAllAttributeKeys(); + assertEquals(baseMessage + "All known fields should be in the iterator", uniqueFields.size(), allAttributes.size()); + assertEquals(baseMessage + "Iterator should have length of string output", stringLength, fpi.getEndIndex()); + int i = 0; + for (char c = fpi.first(); c != AttributedCharacterIterator.DONE; c = fpi.next(), i++) { + Set currentAttributes = fpi.getAttributes().keySet(); + int attributesRemaining = currentAttributes.size(); + for (Object[] cas : expectedFieldPositions) { + Format.Field expectedField = (Format.Field) cas[0]; + int expectedBeginIndex = (Integer) cas[1]; + int expectedEndIndex = (Integer) cas[2]; + if (expectedBeginIndex > i || expectedEndIndex <= i) { + // Field position does not overlap with the current character + continue; + } + + assertTrue(baseMessage + "Character at " + i + " should have field " + expectedField, + currentAttributes.contains(expectedField)); + assertTrue(baseMessage + "Field " + expectedField + " should be a known attribute", + allAttributes.contains(expectedField)); + int actualBeginIndex = fpi.getRunStart(expectedField); + int actualEndIndex = fpi.getRunLimit(expectedField); + assertEquals(baseMessage + expectedField + " begin @" + i, expectedBeginIndex, actualBeginIndex); + assertEquals(baseMessage + expectedField + " end @" + i, expectedEndIndex, actualEndIndex); + attributesRemaining--; + } + assertEquals(baseMessage + "Should have looked at every field", 0, attributesRemaining); + } + assertEquals(baseMessage + "Should have looked at every character", stringLength, i); + + // Check nextPosition over all fields + ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); + i = 0; + for (Object[] cas : expectedFieldPositions) { + assertTrue(baseMessage + i, fv.nextPosition(cfpos)); + Format.Field expectedField = (Format.Field) cas[0]; + int expectedStart = (Integer) cas[1]; + int expectedLimit = (Integer) cas[2]; + assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField()); + assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart()); + assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit()); + i++; + } + assertFalse(baseMessage + "after loop", fv.nextPosition(cfpos)); + + // Check nextPosition constrained over each class one at a time + for (Class classConstraint : uniqueFieldClasses) { + cfpos.reset(); + cfpos.constrainClass(classConstraint); + i = 0; + for (Object[] cas : expectedFieldPositions) { + if (cas[0].getClass() != classConstraint) { + continue; + } + assertTrue(baseMessage + i, fv.nextPosition(cfpos)); + Format.Field expectedField = (Format.Field) cas[0]; + int expectedStart = (Integer) cas[1]; + int expectedLimit = (Integer) cas[2]; + assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField()); + assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart()); + assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit()); + i++; + } + assertFalse(baseMessage + "after loop", fv.nextPosition(cfpos)); + } + + // Check nextPosition constrained over an unrelated class + cfpos.reset(); + cfpos.constrainClass(HashSet.class); + assertFalse(baseMessage + "unrelated class", fv.nextPosition(cfpos)); + + // Check nextPosition constrained over each field one at a time + for (Format.Field field : uniqueFields) { + cfpos.reset(); + cfpos.constrainField(field); + i = 0; + for (Object[] cas : expectedFieldPositions) { + if (cas[0] != field) { + continue; + } + assertTrue(baseMessage + i, fv.nextPosition(cfpos)); + Format.Field expectedField = (Format.Field) cas[0]; + int expectedStart = (Integer) cas[1]; + int expectedLimit = (Integer) cas[2]; + assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField()); + assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart()); + assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit()); + i++; + } + assertFalse(baseMessage + "after loop", fv.nextPosition(cfpos)); + } + } + + public static void assertCharSequenceEquals(CharSequence a, CharSequence b) { + assertEquals(a.toString(), b.toString()); + + assertEquals(a.length(), b.length()); + for (int i = 0; i < a.length(); i++) { + assertEquals(a.charAt(i), b.charAt(i)); + } + + int start = Math.min(2, a.length()); + int end = Math.min(8, a.length()); + if (start != end) { + assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end)); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java index f59e981533c..1545e045798 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java @@ -12,7 +12,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; -import java.text.AttributedCharacterIterator; import java.text.FieldPosition; import java.text.Format; import java.util.HashMap; @@ -24,6 +23,7 @@ import java.util.Set; import org.junit.Ignore; import org.junit.Test; +import com.ibm.icu.dev.test.format.FormattedValueTest; import com.ibm.icu.dev.test.serializable.SerializableTestUtility; import com.ibm.icu.impl.number.Grouper; import com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat; @@ -2164,7 +2164,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.DECIMAL_SEPARATOR, 14, 15}, {NumberFormat.Field.FRACTION, 15, 17}}; - assertFieldPositions(message, fmtd, expectedFieldPositions); + assertNumberFieldPositions(message, fmtd, expectedFieldPositions); // Test the iteration functionality of nextFieldPosition FieldPosition actual = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR); @@ -2211,7 +2211,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.MEASURE_UNIT, 2, 4}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2230,7 +2230,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.MEASURE_UNIT, 2, 6}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2250,7 +2250,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.INTEGER, 0, 2}, // note: field starts after the space {NumberFormat.Field.MEASURE_UNIT, 3, 9}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2270,7 +2270,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.MEASURE_UNIT, 0, 11}, {NumberFormat.Field.INTEGER, 12, 14}, {NumberFormat.Field.MEASURE_UNIT, 15, 19}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2290,7 +2290,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.INTEGER, 0, 2}, // Should trim leading/trailing spaces, but not inner spaces: {NumberFormat.Field.MEASURE_UNIT, 3, 7}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2312,7 +2312,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 1, 3}, {NumberFormat.Field.MEASURE_UNIT, 4, 5}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2331,7 +2331,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.COMPACT, 2, 3}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2350,7 +2350,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.COMPACT, 3, 11}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2369,7 +2369,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 0, 1}, {NumberFormat.Field.COMPACT, 2, 9}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2388,7 +2388,7 @@ public class NumberFormatterApiTest { // field, begin index, end index {NumberFormat.Field.INTEGER, 1, 2}, {NumberFormat.Field.COMPACT, 3, 6}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2408,7 +2408,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.COMPACT, 3, 8}, {NumberFormat.Field.CURRENCY, 9, 12}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2430,7 +2430,7 @@ public class NumberFormatterApiTest { {NumberFormat.Field.INTEGER, 0, 2}, {NumberFormat.Field.COMPACT, 3, 11}, {NumberFormat.Field.MEASURE_UNIT, 12, 18}}; - assertFieldPositions( + assertNumberFieldPositions( message, result, expectedFieldPositions); @@ -2726,43 +2726,7 @@ public class NumberFormatterApiTest { } catch (UnsupportedOperationException expected) {} } - static void assertFieldPositions(String message, FormattedNumber formattedNumber, Object[][] expectedFieldPositions) { - // Calculate some initial expected values - int stringLength = formattedNumber.toString().length(); - HashSet uniqueFields = new HashSet<>(); - for (int i=0; i allAttributes = fpi.getAllAttributeKeys(); - assertEquals(baseMessage + "All known fields should be in the iterator", uniqueFields.size(), allAttributes.size()); - assertEquals(baseMessage + "Iterator should have length of string output", stringLength, fpi.getEndIndex()); - int i = 0; - for (char c = fpi.first(); c != AttributedCharacterIterator.DONE; c = fpi.next(), i++) { - Set currentAttributes = fpi.getAttributes().keySet(); - int attributesRemaining = currentAttributes.size(); - for (Object[] cas : expectedFieldPositions) { - NumberFormat.Field expectedField = (NumberFormat.Field) cas[0]; - int expectedBeginIndex = (Integer) cas[1]; - int expectedEndIndex = (Integer) cas[2]; - if (expectedBeginIndex > i || expectedEndIndex <= i) { - // Field position does not overlap with the current character - continue; - } - - assertTrue(baseMessage + "Current character should have expected field", currentAttributes.contains(expectedField)); - assertTrue(baseMessage + "Field should be a known attribute", allAttributes.contains(expectedField)); - int actualBeginIndex = fpi.getRunStart(expectedField); - int actualEndIndex = fpi.getRunLimit(expectedField); - assertEquals(baseMessage + expectedField + " begin @" + i, expectedBeginIndex, actualBeginIndex); - assertEquals(baseMessage + expectedField + " end @" + i, expectedEndIndex, actualEndIndex); - attributesRemaining--; - } - assertEquals(baseMessage + "Should have looked at every field", 0, attributesRemaining); - } - assertEquals(baseMessage + "Should have looked at every character", stringLength, i); + private void assertNumberFieldPositions(String message, FormattedNumber result, Object[][] expectedFieldPositions) { + FormattedValueTest.checkFormattedValue(message, result, result.toString(), expectedFieldPositions); } } 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 index 367d8df409c..5c4d53e632c 100644 --- 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 @@ -8,6 +8,8 @@ import java.util.Locale; import org.junit.Test; +import com.ibm.icu.dev.test.format.FormattedValueTest; +import com.ibm.icu.number.FormattedNumberRange; import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.LocalizedNumberRangeFormatter; import com.ibm.icu.number.Notation; @@ -19,6 +21,7 @@ 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.text.NumberFormat; import com.ibm.icu.util.Currency; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; @@ -706,6 +709,50 @@ public class NumberRangeFormatterTest { } } + @Test + public void testFieldPositions() { + { + String message = "Field position test 1"; + String expectedString = "3K – 5K m"; + FormattedNumberRange fmtd = assertFormattedRangeEquals( + message, + NumberRangeFormatter.with() + .numberFormatterBoth(NumberFormatter.with() + .unit(MeasureUnit.METER) + .notation(Notation.compactShort())) + .locale(ULocale.US), + 3000, + 5000, + expectedString); + Object[][] expectedFieldPositions = new Object[][]{ + {NumberFormat.Field.INTEGER, 0, 1}, + {NumberFormat.Field.COMPACT, 1, 2}, + {NumberFormat.Field.INTEGER, 5, 6}, + {NumberFormat.Field.COMPACT, 6, 7}, + {NumberFormat.Field.MEASURE_UNIT, 8, 9}}; + FormattedValueTest.checkFormattedValue(message, fmtd, expectedString, expectedFieldPositions); + } + + { + String message = "Field position test 2"; + String expectedString = "87,654,321–98,765,432"; + FormattedNumberRange fmtd = assertFormattedRangeEquals( + message, + NumberRangeFormatter.withLocale(ULocale.US), + 87654321, + 98765432, + expectedString); + Object[][] expectedFieldPositions = new Object[][]{ + {NumberFormat.Field.GROUPING_SEPARATOR, 2, 3}, + {NumberFormat.Field.GROUPING_SEPARATOR, 6, 7}, + {NumberFormat.Field.INTEGER, 0, 10}, + {NumberFormat.Field.GROUPING_SEPARATOR, 13, 14}, + {NumberFormat.Field.GROUPING_SEPARATOR, 17, 18}, + {NumberFormat.Field.INTEGER, 11, 21}}; + FormattedValueTest.checkFormattedValue(message, fmtd, expectedString, expectedFieldPositions); + } + } + static void assertFormatRange( String message, UnlocalizedNumberRangeFormatter f, @@ -733,10 +780,12 @@ public class NumberRangeFormatterTest { assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M); } - private static void assertFormattedRangeEquals(String message, LocalizedNumberRangeFormatter l, Number first, + private static FormattedNumberRange assertFormattedRangeEquals(String message, LocalizedNumberRangeFormatter l, Number first, Number second, String expected) { - String actual = l.formatRange(first, second).toString(); + FormattedNumberRange fnr = l.formatRange(first, second); + String actual = fnr.toString(); assertEquals(message + ": " + first + ", " + second, expected, actual); + return fnr; } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java index e838d30b5fc..c85bbfe5199 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java @@ -262,6 +262,10 @@ public class NumberStringBuilderTest { int end = Math.min(12, a.length()); if (start != end) { assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end)); + if (b instanceof NumberStringBuilder) { + NumberStringBuilder bnsb = (NumberStringBuilder) b; + assertCharSequenceEquals(a.subSequence(start, end), bnsb.subString(start, end)); + } } } }