From 249e03ccd6909a7528afdd0cbf8814a0ae9e3801 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Fri, 8 Feb 2019 22:08:16 -0800 Subject: [PATCH] ICU-13256 Implementing FormattedRelativeDateTime in C, C++, and Java. - Adds additional logic to NumberStringBuilder. - Extends logic of number::impl::Field type. - Adds tests for RBNF support. - Adds tests from ftang's original PR. --- icu4c/source/i18n/Makefile.in | 2 +- icu4c/source/i18n/formattedval_impl.h | 26 +- icu4c/source/i18n/formattedval_sbimpl.cpp | 46 +++ icu4c/source/i18n/formattedvalue.cpp | 2 +- icu4c/source/i18n/i18n.vcxproj | 1 + icu4c/source/i18n/i18n.vcxproj.filters | 3 + icu4c/source/i18n/i18n_uwp.vcxproj | 1 + icu4c/source/i18n/number_affixutils.cpp | 20 +- icu4c/source/i18n/number_modifiers.cpp | 10 +- icu4c/source/i18n/number_modifiers.h | 2 +- icu4c/source/i18n/number_output.cpp | 2 +- icu4c/source/i18n/number_stringbuilder.cpp | 59 ++- icu4c/source/i18n/number_stringbuilder.h | 4 +- icu4c/source/i18n/number_types.h | 7 +- icu4c/source/i18n/number_utils.h | 42 ++ icu4c/source/i18n/numrange_fluent.cpp | 2 +- icu4c/source/i18n/quantityformatter.cpp | 35 ++ icu4c/source/i18n/quantityformatter.h | 28 ++ icu4c/source/i18n/reldatefmt.cpp | 338 +++++++++++++--- icu4c/source/i18n/unicode/formattedvalue.h | 2 +- icu4c/source/i18n/unicode/reldatefmt.h | 206 +++++++++- icu4c/source/i18n/unicode/uformattedvalue.h | 7 + icu4c/source/i18n/unicode/unumberformatter.h | 14 +- icu4c/source/i18n/unicode/ureldatefmt.h | 145 +++++++ .../test/cintltst/crelativedateformattest.c | 372 +++++++++++++++++- icu4c/source/test/depstest/dependencies.txt | 7 +- .../intltest/numbertest_stringbuilder.cpp | 8 +- icu4c/source/test/intltest/reldatefmttest.cpp | 140 ++++++- .../impl/number/ConstantAffixModifier.java | 2 +- .../number/ConstantMultiFieldModifier.java | 3 +- .../CurrencySpacingEnabledModifier.java | 4 +- .../src/com/ibm/icu/impl/number/Modifier.java | 3 +- .../impl/number/MutablePatternModifier.java | 2 +- .../icu/impl/number/NumberStringBuilder.java | 61 ++- .../ibm/icu/impl/number/SimpleModifier.java | 8 +- .../com/ibm/icu/number/FormattedNumber.java | 4 +- .../ibm/icu/number/FormattedNumberRange.java | 4 +- .../icu/number/LocalizedNumberFormatter.java | 14 +- .../ibm/icu/number/ScientificNotation.java | 3 +- .../src/com/ibm/icu/text/NumberFormat.java | 4 + .../com/ibm/icu/text/QuantityFormatter.java | 21 +- .../icu/text/RelativeDateTimeFormatter.java | 346 +++++++++++++++- .../dev/test/format/FormattedValueTest.java | 5 +- .../format/RelativeDateTimeFormatterTest.java | 103 ++++- .../dev/test/number/DecimalQuantityTest.java | 9 +- .../dev/test/serializable/FormatHandler.java | 16 + .../serializable/SerializableTestUtility.java | 1 + 47 files changed, 1936 insertions(+), 208 deletions(-) create mode 100644 icu4c/source/i18n/formattedval_sbimpl.cpp diff --git a/icu4c/source/i18n/Makefile.in b/icu4c/source/i18n/Makefile.in index d030efa8a7d..2d001879e8f 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 formattedvalue.o formattedval_iterimpl.o +erarules.o formattedvalue.o formattedval_iterimpl.o formattedval_sbimpl.o ## Header files to install HEADERS = $(srcdir)/unicode/*.h diff --git a/icu4c/source/i18n/formattedval_impl.h b/icu4c/source/i18n/formattedval_impl.h index d3b49ecea08..b08a3e61d4d 100644 --- a/icu4c/source/i18n/formattedval_impl.h +++ b/icu4c/source/i18n/formattedval_impl.h @@ -18,6 +18,7 @@ #include "fphdlimp.h" #include "util.h" #include "uvectr32.h" +#include "number_stringbuilder.h" U_NAMESPACE_BEGIN @@ -44,12 +45,35 @@ public: void appendString(UnicodeString string, UErrorCode& status); private: - // Final data: UnicodeString fString; UVector32 fFields; }; +class FormattedValueNumberStringBuilderImpl : public UMemory, public FormattedValue { +public: + + FormattedValueNumberStringBuilderImpl(number::impl::Field numericField); + + virtual ~FormattedValueNumberStringBuilderImpl(); + + // Implementation of FormattedValue (const): + + UnicodeString toString(UErrorCode& status) const U_OVERRIDE; + UnicodeString toTempString(UErrorCode& status) const U_OVERRIDE; + Appendable& appendTo(Appendable& appendable, UErrorCode& status) const U_OVERRIDE; + UBool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const U_OVERRIDE; + + inline number::impl::NumberStringBuilder& getStringRef() { + return fString; + } + +private: + number::impl::NumberStringBuilder fString; + number::impl::Field fNumericField; +}; + + // C API Helpers for FormattedValue // Magic number as ASCII == "UFV" struct UFormattedValueImpl; diff --git a/icu4c/source/i18n/formattedval_sbimpl.cpp b/icu4c/source/i18n/formattedval_sbimpl.cpp new file mode 100644 index 00000000000..1fbecf25ac6 --- /dev/null +++ b/icu4c/source/i18n/formattedval_sbimpl.cpp @@ -0,0 +1,46 @@ +// © 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 + +// This file contains one implementation of FormattedValue. +// Other independent implementations should go into their own cpp file for +// better dependency modularization. + +#include "formattedval_impl.h" + +U_NAMESPACE_BEGIN + + +FormattedValueNumberStringBuilderImpl::FormattedValueNumberStringBuilderImpl(number::impl::Field numericField) + : fNumericField(numericField) { +} + +FormattedValueNumberStringBuilderImpl::~FormattedValueNumberStringBuilderImpl() { +} + + +UnicodeString FormattedValueNumberStringBuilderImpl::toString(UErrorCode&) const { + return fString.toUnicodeString(); +} + +UnicodeString FormattedValueNumberStringBuilderImpl::toTempString(UErrorCode&) const { + return fString.toTempUnicodeString(); +} + +Appendable& FormattedValueNumberStringBuilderImpl::appendTo(Appendable& appendable, UErrorCode&) const { + appendable.appendString(fString.chars(), fString.length()); + return appendable; +} + +UBool FormattedValueNumberStringBuilderImpl::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const { + // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool + return fString.nextPosition(cfpos, fNumericField, status) ? TRUE : FALSE; +} + + +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/formattedvalue.cpp b/icu4c/source/i18n/formattedvalue.cpp index 4d33a4d4d2a..17bec7b876f 100644 --- a/icu4c/source/i18n/formattedvalue.cpp +++ b/icu4c/source/i18n/formattedvalue.cpp @@ -51,7 +51,7 @@ void ConstrainedFieldPosition::setState( fLimit = limit; } -UBool ConstrainedFieldPosition::matchesField(UFieldCategory category, int32_t field) { +UBool ConstrainedFieldPosition::matchesField(int32_t category, int32_t field) { switch (fConstraint) { case UCFPOS_CONSTRAINT_NONE: return TRUE; diff --git a/icu4c/source/i18n/i18n.vcxproj b/icu4c/source/i18n/i18n.vcxproj index 6072a1c3e2c..8a53977eafe 100644 --- a/icu4c/source/i18n/i18n.vcxproj +++ b/icu4c/source/i18n/i18n.vcxproj @@ -239,6 +239,7 @@ + diff --git a/icu4c/source/i18n/i18n.vcxproj.filters b/icu4c/source/i18n/i18n.vcxproj.filters index 6227485f26a..5cf379952d8 100644 --- a/icu4c/source/i18n/i18n.vcxproj.filters +++ b/icu4c/source/i18n/i18n.vcxproj.filters @@ -159,6 +159,9 @@ formatting + + formatting + formatting diff --git a/icu4c/source/i18n/i18n_uwp.vcxproj b/icu4c/source/i18n/i18n_uwp.vcxproj index bde119f895c..20c43aa5533 100644 --- a/icu4c/source/i18n/i18n_uwp.vcxproj +++ b/icu4c/source/i18n/i18n_uwp.vcxproj @@ -346,6 +346,7 @@ + diff --git a/icu4c/source/i18n/number_affixutils.cpp b/icu4c/source/i18n/number_affixutils.cpp index d4588bff816..3eb9c59bf49 100644 --- a/icu4c/source/i18n/number_affixutils.cpp +++ b/icu4c/source/i18n/number_affixutils.cpp @@ -131,25 +131,25 @@ UnicodeString AffixUtils::escape(const UnicodeString &input) { Field AffixUtils::getFieldForType(AffixPatternType type) { switch (type) { case TYPE_MINUS_SIGN: - return Field::UNUM_SIGN_FIELD; + return UNUM_SIGN_FIELD; case TYPE_PLUS_SIGN: - return Field::UNUM_SIGN_FIELD; + return UNUM_SIGN_FIELD; case TYPE_PERCENT: - return Field::UNUM_PERCENT_FIELD; + return UNUM_PERCENT_FIELD; case TYPE_PERMILLE: - return Field::UNUM_PERMILL_FIELD; + return UNUM_PERMILL_FIELD; case TYPE_CURRENCY_SINGLE: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; case TYPE_CURRENCY_DOUBLE: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; case TYPE_CURRENCY_TRIPLE: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; case TYPE_CURRENCY_QUAD: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; case TYPE_CURRENCY_QUINT: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; case TYPE_CURRENCY_OVERFLOW: - return Field::UNUM_CURRENCY_FIELD; + return UNUM_CURRENCY_FIELD; default: UPRV_UNREACHABLE; } diff --git a/icu4c/source/i18n/number_modifiers.cpp b/icu4c/source/i18n/number_modifiers.cpp index 5875bbd6826..1fcbe7b9b79 100644 --- a/icu4c/source/i18n/number_modifiers.cpp +++ b/icu4c/source/i18n/number_modifiers.cpp @@ -156,7 +156,7 @@ SimpleModifier::SimpleModifier() int32_t SimpleModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex, UErrorCode &status) const { - return formatAsPrefixSuffix(output, leftIndex, rightIndex, fField, status); + return formatAsPrefixSuffix(output, leftIndex, rightIndex, status); } int32_t SimpleModifier::getPrefixLength() const { @@ -204,13 +204,13 @@ bool SimpleModifier::semanticallyEquivalent(const Modifier& other) const { int32_t SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startIndex, int32_t endIndex, - Field field, UErrorCode &status) const { + UErrorCode &status) const { if (fSuffixOffset == -1 && fPrefixLength + fSuffixLength > 0) { // There is no argument for the inner number; overwrite the entire segment with our string. - return result.splice(startIndex, endIndex, fCompiledPattern, 2, 2 + fPrefixLength, field, status); + return result.splice(startIndex, endIndex, fCompiledPattern, 2, 2 + fPrefixLength, fField, status); } else { if (fPrefixLength > 0) { - result.insert(startIndex, fCompiledPattern, 2, 2 + fPrefixLength, field, status); + result.insert(startIndex, fCompiledPattern, 2, 2 + fPrefixLength, fField, status); } if (fSuffixLength > 0) { result.insert( @@ -218,7 +218,7 @@ SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startI fCompiledPattern, 1 + fSuffixOffset, 1 + fSuffixOffset + fSuffixLength, - field, + fField, status); } return fPrefixLength + fSuffixLength; diff --git a/icu4c/source/i18n/number_modifiers.h b/icu4c/source/i18n/number_modifiers.h index 65ada937d03..495128bb149 100644 --- a/icu4c/source/i18n/number_modifiers.h +++ b/icu4c/source/i18n/number_modifiers.h @@ -100,7 +100,7 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory { * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. */ int32_t - formatAsPrefixSuffix(NumberStringBuilder& result, int32_t startIndex, int32_t endIndex, Field field, + formatAsPrefixSuffix(NumberStringBuilder& result, int32_t startIndex, int32_t endIndex, UErrorCode& status) const; /** diff --git a/icu4c/source/i18n/number_output.cpp b/icu4c/source/i18n/number_output.cpp index e39f5756845..378b7b075e2 100644 --- a/icu4c/source/i18n/number_output.cpp +++ b/icu4c/source/i18n/number_output.cpp @@ -74,7 +74,7 @@ UBool FormattedNumber::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& return FALSE; } // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool - return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; + return fResults->string.nextPosition(cfpos, 0, status) ? TRUE : FALSE; } UBool FormattedNumber::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const { diff --git a/icu4c/source/i18n/number_stringbuilder.cpp b/icu4c/source/i18n/number_stringbuilder.cpp index f756edc28e6..03300b33ac5 100644 --- a/icu4c/source/i18n/number_stringbuilder.cpp +++ b/icu4c/source/i18n/number_stringbuilder.cpp @@ -8,6 +8,7 @@ #include "number_stringbuilder.h" #include "static_unicode_sets.h" #include "unicode/utf16.h" +#include "number_utils.h" using namespace icu; using namespace icu::number; @@ -41,7 +42,7 @@ NumberStringBuilder::NumberStringBuilder() { getCharPtr()[i] = 1; } #endif -}; +} NumberStringBuilder::~NumberStringBuilder() { if (fUsingHeap) { @@ -449,7 +450,7 @@ bool NumberStringBuilder::nextFieldPosition(FieldPosition& fp, UErrorCode& statu ConstrainedFieldPosition cfpos; cfpos.constrainField(UFIELD_CATEGORY_NUMBER, rawField); cfpos.setState(UFIELD_CATEGORY_NUMBER, rawField, fp.getBeginIndex(), fp.getEndIndex()); - if (nextPosition(cfpos, status)) { + if (nextPosition(cfpos, 0, status)) { fp.setBeginIndex(cfpos.getStart()); fp.setEndIndex(cfpos.getLimit()); return true; @@ -476,25 +477,21 @@ bool NumberStringBuilder::nextFieldPosition(FieldPosition& fp, UErrorCode& statu void NumberStringBuilder::getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const { ConstrainedFieldPosition cfpos; - while (nextPosition(cfpos, status)) { + while (nextPosition(cfpos, 0, 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; - } +// Signal the end of the string using a field that doesn't exist and that is +// different from UNUM_FIELD_COUNT, which is used for "null number field". +static constexpr Field kEndField = 0xff; +bool NumberStringBuilder::nextPosition(ConstrainedFieldPosition& cfpos, Field numericField, UErrorCode& /*status*/) const { + auto numericCAF = NumFieldUtils::expand(numericField); int32_t fieldStart = -1; - int32_t currField = UNUM_FIELD_COUNT; + Field 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; + Field _field = (i < fZero + fLength) ? getFieldPtr()[i] : kEndField; // Case 1: currently scanning a field. if (currField != UNUM_FIELD_COUNT) { if (currField != _field) { @@ -514,15 +511,17 @@ bool NumberStringBuilder::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCo if (currField != UNUM_GROUPING_SEPARATOR_FIELD) { start = trimFront(start); } - cfpos.setState(UFIELD_CATEGORY_NUMBER, currField, start, end); + auto caf = NumFieldUtils::expand(currField); + cfpos.setState(caf.category, caf.field, start, end); return true; } continue; } // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER. - if ((!isSearchingForField || cfpos.getField() == UNUM_INTEGER_FIELD) + if (cfpos.matchesField(UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD) && i > fZero - && i - fZero > cfpos.getLimit() // don't return the same field twice in a row + // don't return the same field twice in a row: + && i - fZero > cfpos.getLimit() && isIntOrGroup(getFieldPtr()[i - 1]) && !isIntOrGroup(_field)) { int j = i - 1; @@ -530,16 +529,32 @@ bool NumberStringBuilder::nextPosition(ConstrainedFieldPosition& cfpos, UErrorCo cfpos.setState(UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, j - fZero + 1, i - fZero); return true; } + // Special case: coalesce NUMERIC if we are pointing at the end of the NUMERIC. + if (numericField != 0 + && cfpos.matchesField(numericCAF.category, numericCAF.field) + && i > fZero + // don't return the same field twice in a row: + && (i - fZero > cfpos.getLimit() + || cfpos.getCategory() != numericCAF.category + || cfpos.getField() != numericCAF.field) + && isNumericField(getFieldPtr()[i - 1]) + && !isNumericField(_field)) { + int j = i - 1; + for (; j >= fZero && isNumericField(getFieldPtr()[j]); j--) {} + cfpos.setState(numericCAF.category, numericCAF.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) { + if (_field == UNUM_FIELD_COUNT || _field == kEndField) { continue; } // Case 3: check for field starting at this position - if (!isSearchingForField || cfpos.getField() == _field) { + auto caf = NumFieldUtils::expand(_field); + if (cfpos.matchesField(caf.category, caf.field)) { fieldStart = i - fZero; currField = _field; } @@ -560,7 +575,11 @@ bool NumberStringBuilder::containsField(Field field) const { bool NumberStringBuilder::isIntOrGroup(Field field) { return field == UNUM_INTEGER_FIELD - || field ==UNUM_GROUPING_SEPARATOR_FIELD; + || field == UNUM_GROUPING_SEPARATOR_FIELD; +} + +bool NumberStringBuilder::isNumericField(Field field) { + return NumFieldUtils::isNumericField(field); } int32_t NumberStringBuilder::trimBack(int32_t limit) const { diff --git a/icu4c/source/i18n/number_stringbuilder.h b/icu4c/source/i18n/number_stringbuilder.h index ca3d6be417f..d48f6e106cf 100644 --- a/icu4c/source/i18n/number_stringbuilder.h +++ b/icu4c/source/i18n/number_stringbuilder.h @@ -108,7 +108,7 @@ class U_I18N_API NumberStringBuilder : public UMemory { void getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const; - bool nextPosition(ConstrainedFieldPosition& cfpos, UErrorCode& status) const; + bool nextPosition(ConstrainedFieldPosition& cfpos, Field numericField, UErrorCode& status) const; bool containsField(Field field) const; @@ -147,6 +147,8 @@ class U_I18N_API NumberStringBuilder : public UMemory { static bool isIntOrGroup(Field field); + static bool isNumericField(Field field); + int32_t trimBack(int32_t limit) const; int32_t trimFront(int32_t start) const; diff --git a/icu4c/source/i18n/number_types.h b/icu4c/source/i18n/number_types.h index 00a6818869f..225d1e57750 100644 --- a/icu4c/source/i18n/number_types.h +++ b/icu4c/source/i18n/number_types.h @@ -23,7 +23,11 @@ namespace impl { // Typedef several enums for brevity and for easier comparison to Java. -typedef UNumberFormatFields Field; +// Convention: bottom 4 bits for field, top 4 bits for field category. +// Field category 0 implies the number category so that the number field +// literals can be directly passed as a Field type. +// See the helper functions in "NumFieldUtils" in number_utils.h +typedef uint8_t Field; typedef UNumberFormatRoundingMode RoundingMode; @@ -346,6 +350,7 @@ class U_I18N_API NullableValue { T fValue; }; + } // namespace impl } // namespace number U_NAMESPACE_END diff --git a/icu4c/source/i18n/number_utils.h b/icu4c/source/i18n/number_utils.h index c367166009c..05b8ec02a52 100644 --- a/icu4c/source/i18n/number_utils.h +++ b/icu4c/source/i18n/number_utils.h @@ -32,6 +32,48 @@ enum CldrPatternStyle { CLDR_PATTERN_STYLE_COUNT, }; + +/** + * Helper functions for dealing with the Field typedef, which stores fields + * in a compressed format. + */ +class NumFieldUtils { +public: + struct CategoryFieldPair { + int32_t category; + int32_t field; + }; + + /** Compile-time function to construct a Field from a category and a field */ + template + static constexpr Field compress() { + static_assert(category != 0, "cannot use Undefined category in NumFieldUtils"); + static_assert(category <= 0xf, "only 4 bits for category"); + static_assert(field <= 0xf, "only 4 bits for field"); + return static_cast((category << 4) | field); + } + + /** Runtime inline function to unpack the category and field from the Field */ + static inline CategoryFieldPair expand(Field field) { + if (field == UNUM_FIELD_COUNT) { + return {UFIELD_CATEGORY_UNDEFINED, 0}; + } + CategoryFieldPair ret = { + (field >> 4), + (field & 0xf) + }; + if (ret.category == 0) { + ret.category = UFIELD_CATEGORY_NUMBER; + } + return ret; + } + + static inline bool isNumericField(Field field) { + int8_t category = field >> 4; + return category == 0 || category == UFIELD_CATEGORY_NUMBER; + } +}; + // Namespace for naked functions namespace utils { diff --git a/icu4c/source/i18n/numrange_fluent.cpp b/icu4c/source/i18n/numrange_fluent.cpp index 10479f9d106..929cac996cf 100644 --- a/icu4c/source/i18n/numrange_fluent.cpp +++ b/icu4c/source/i18n/numrange_fluent.cpp @@ -435,7 +435,7 @@ UBool FormattedNumberRange::nextPosition(ConstrainedFieldPosition& cfpos, UError return FALSE; } // NOTE: MSVC sometimes complains when implicitly converting between bool and UBool - return fResults->string.nextPosition(cfpos, status) ? TRUE : FALSE; + return fResults->string.nextPosition(cfpos, 0, status) ? TRUE : FALSE; } UBool FormattedNumberRange::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const { diff --git a/icu4c/source/i18n/quantityformatter.cpp b/icu4c/source/i18n/quantityformatter.cpp index ba06ba06b97..7235fa6d9d6 100644 --- a/icu4c/source/i18n/quantityformatter.cpp +++ b/icu4c/source/i18n/quantityformatter.cpp @@ -25,6 +25,8 @@ #include "standardplural.h" #include "uassert.h" #include "number_decimalquantity.h" +#include "number_utypes.h" +#include "number_stringbuilder.h" U_NAMESPACE_BEGIN @@ -174,6 +176,39 @@ StandardPlural::Form QuantityFormatter::selectPlural( return StandardPlural::orOtherFromString(pluralKeyword); } +void QuantityFormatter::formatAndSelect( + double quantity, + const NumberFormat& fmt, + const PluralRules& rules, + number::impl::NumberStringBuilder& output, + StandardPlural::Form& pluralForm, + UErrorCode& status) { + UnicodeString pluralKeyword; + const DecimalFormat* df = dynamic_cast(&fmt); + if (df != nullptr) { + number::impl::UFormattedNumberData fn; + fn.quantity.setToDouble(quantity); + df->toNumberFormatter().formatImpl(&fn, status); + if (U_FAILURE(status)) { + return; + } + output = std::move(fn.string); + pluralKeyword = rules.select(fn.quantity); + } else { + UnicodeString result; + fmt.format(quantity, result, status); + if (U_FAILURE(status)) { + return; + } + output.append(result, UNUM_FIELD_COUNT, status); + if (U_FAILURE(status)) { + return; + } + pluralKeyword = rules.select(quantity); + } + pluralForm = StandardPlural::orOtherFromString(pluralKeyword); +} + UnicodeString &QuantityFormatter::format( const SimpleFormatter &pattern, const UnicodeString &value, diff --git a/icu4c/source/i18n/quantityformatter.h b/icu4c/source/i18n/quantityformatter.h index ca3fb3d83b8..0069cb2b8de 100644 --- a/icu4c/source/i18n/quantityformatter.h +++ b/icu4c/source/i18n/quantityformatter.h @@ -27,6 +27,12 @@ class NumberFormat; class Formattable; class FieldPosition; +namespace number { +namespace impl { +class NumberStringBuilder; +} +} + /** * A plural aware formatter that is good for expressing a single quantity and * a unit. @@ -111,6 +117,7 @@ public: /** * Selects the standard plural form for the number/formatter/rules. + * TODO(13591): Remove this method. */ static StandardPlural::Form selectPlural( const Formattable &number, @@ -120,6 +127,27 @@ public: FieldPosition &pos, UErrorCode &status); + /** + * Formats a quantity and selects its plural form. The output is appended + * to a NumberStringBuilder in order to retain field information. + * + * @param quantity The number to format. + * @param fmt The formatter to use to format the number. + * @param rules The rules to use to select the plural form of the + * formatted number. + * @param output Where to append the result of the format operation. + * @param pluralForm Output variable populated with the plural form of the + * formatted number. + * @param status Set if an error occurs. + */ + static void formatAndSelect( + double quantity, + const NumberFormat& fmt, + const PluralRules& rules, + number::impl::NumberStringBuilder& output, + StandardPlural::Form& pluralForm, + UErrorCode& status); + /** * Formats the pattern with the value and adjusts the FieldPosition. */ diff --git a/icu4c/source/i18n/reldatefmt.cpp b/icu4c/source/i18n/reldatefmt.cpp index 06c52bd6c6c..4f6f7233ddb 100644 --- a/icu4c/source/i18n/reldatefmt.cpp +++ b/icu4c/source/i18n/reldatefmt.cpp @@ -15,6 +15,7 @@ #if !UCONFIG_NO_FORMATTING && !UCONFIG_NO_BREAK_ITERATION #include +#include #include "unicode/dtfmtsym.h" #include "unicode/ucasemap.h" #include "unicode/ureldatefmt.h" @@ -41,6 +42,12 @@ #include "sharednumberformat.h" #include "standardplural.h" #include "unifiedcache.h" +#include "util.h" +#include "number_stringbuilder.h" +#include "number_utypes.h" +#include "number_modifiers.h" +#include "formattedval_impl.h" +#include "number_utils.h" // Copied from uscript_props.cpp @@ -717,6 +724,26 @@ const RelativeDateTimeCacheData *LocaleCacheKey::crea return result.orphan(); } + + +static constexpr number::impl::Field kRDTNumericField + = number::impl::NumFieldUtils::compress(); + +static constexpr number::impl::Field kRDTLiteralField + = number::impl::NumFieldUtils::compress(); + +class FormattedRelativeDateTimeData : public FormattedValueNumberStringBuilderImpl { +public: + FormattedRelativeDateTimeData() : FormattedValueNumberStringBuilderImpl(kRDTNumericField) {} + virtual ~FormattedRelativeDateTimeData(); +}; + +FormattedRelativeDateTimeData::~FormattedRelativeDateTimeData() = default; + + +UPRV_FORMATTED_VALUE_SUBCLASS_AUTO_IMPL(FormattedRelativeDateTime) + + RelativeDateTimeFormatter::RelativeDateTimeFormatter(UErrorCode& status) : fCache(nullptr), fNumberFormat(nullptr), @@ -841,43 +868,142 @@ UDateRelativeDateTimeFormatterStyle RelativeDateTimeFormatter::getFormatStyle() return fStyle; } -UnicodeString& RelativeDateTimeFormatter::format( - double quantity, UDateDirection direction, UDateRelativeUnit unit, - UnicodeString& appendTo, UErrorCode& status) const { + +// To reduce boilerplate code, we use a helper function that forwards variadic +// arguments to the formatImpl function. + +template +UnicodeString& RelativeDateTimeFormatter::doFormat( + F callback, + UnicodeString& appendTo, + UErrorCode& status, + Args... args) const { + FormattedRelativeDateTimeData output; + (this->*callback)(std::forward(args)..., output, status); if (U_FAILURE(status)) { return appendTo; } + UnicodeString result = output.getStringRef().toUnicodeString(); + return appendTo.append(adjustForContext(result)); +} + +template +FormattedRelativeDateTime RelativeDateTimeFormatter::doFormatToValue( + F callback, + UErrorCode& status, + Args... args) const { + if (!checkNoAdjustForContext(status)) { + return FormattedRelativeDateTime(status); + } + LocalPointer output( + new FormattedRelativeDateTimeData(), status); + if (U_FAILURE(status)) { + return FormattedRelativeDateTime(status); + } + (this->*callback)(std::forward(args)..., *output, status); + output->getStringRef().writeTerminator(status); + return FormattedRelativeDateTime(output.orphan()); +} + +UnicodeString& RelativeDateTimeFormatter::format( + double quantity, + UDateDirection direction, + UDateRelativeUnit unit, + UnicodeString& appendTo, + UErrorCode& status) const { + return doFormat( + &RelativeDateTimeFormatter::formatImpl, + appendTo, + status, + quantity, + direction, + unit); +} + +FormattedRelativeDateTime RelativeDateTimeFormatter::formatToValue( + double quantity, + UDateDirection direction, + UDateRelativeUnit unit, + UErrorCode& status) const { + return doFormatToValue( + &RelativeDateTimeFormatter::formatImpl, + status, + quantity, + direction, + unit); +} + +void RelativeDateTimeFormatter::formatImpl( + double quantity, + UDateDirection direction, + UDateRelativeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const { + if (U_FAILURE(status)) { + return; + } if (direction != UDAT_DIRECTION_LAST && direction != UDAT_DIRECTION_NEXT) { status = U_ILLEGAL_ARGUMENT_ERROR; - return appendTo; + return; } int32_t bFuture = direction == UDAT_DIRECTION_NEXT ? 1 : 0; - FieldPosition pos(FieldPosition::DONT_CARE); - UnicodeString result; - UnicodeString formattedNumber; - - StandardPlural::Form pluralIndex = QuantityFormatter::selectPlural( - quantity, **fNumberFormat, **fPluralRules, formattedNumber, pos, + StandardPlural::Form pluralForm; + QuantityFormatter::formatAndSelect( + quantity, + **fNumberFormat, + **fPluralRules, + output.getStringRef(), + pluralForm, status); + if (U_FAILURE(status)) { + return; + } const SimpleFormatter* formatter = - fCache->getRelativeUnitFormatter(fStyle, unit, bFuture, pluralIndex); + fCache->getRelativeUnitFormatter(fStyle, unit, bFuture, pluralForm); if (formatter == nullptr) { // TODO: WARN - look at quantity formatter's action with an error. status = U_INVALID_FORMAT_ERROR; - return appendTo; + return; } - formatter->format(formattedNumber, result, status); - adjustForContext(result); - return appendTo.append(result); + + number::impl::SimpleModifier modifier(*formatter, kRDTLiteralField, false); + modifier.formatAsPrefixSuffix( + output.getStringRef(), 0, output.getStringRef().length(), status); } UnicodeString& RelativeDateTimeFormatter::formatNumeric( - double offset, URelativeDateTimeUnit unit, - UnicodeString& appendTo, UErrorCode& status) const { + double offset, + URelativeDateTimeUnit unit, + UnicodeString& appendTo, + UErrorCode& status) const { + return doFormat( + &RelativeDateTimeFormatter::formatNumericImpl, + appendTo, + status, + offset, + unit); +} + +FormattedRelativeDateTime RelativeDateTimeFormatter::formatNumericToValue( + double offset, + URelativeDateTimeUnit unit, + UErrorCode& status) const { + return doFormatToValue( + &RelativeDateTimeFormatter::formatNumericImpl, + status, + offset, + unit); +} + +void RelativeDateTimeFormatter::formatNumericImpl( + double offset, + URelativeDateTimeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const { if (U_FAILURE(status)) { - return appendTo; + return; } UDateDirection direction = UDAT_DIRECTION_NEXT; if (std::signbit(offset)) { // needed to handle -0.0 @@ -886,55 +1012,110 @@ UnicodeString& RelativeDateTimeFormatter::formatNumeric( } if (direction != UDAT_DIRECTION_LAST && direction != UDAT_DIRECTION_NEXT) { status = U_ILLEGAL_ARGUMENT_ERROR; - return appendTo; + return; } int32_t bFuture = direction == UDAT_DIRECTION_NEXT ? 1 : 0; - FieldPosition pos(FieldPosition::DONT_CARE); - UnicodeString result; - UnicodeString formattedNumber; - - StandardPlural::Form pluralIndex = QuantityFormatter::selectPlural( - offset, **fNumberFormat, **fPluralRules, formattedNumber, pos, + StandardPlural::Form pluralForm; + QuantityFormatter::formatAndSelect( + offset, + **fNumberFormat, + **fPluralRules, + output.getStringRef(), + pluralForm, status); + if (U_FAILURE(status)) { + return; + } const SimpleFormatter* formatter = - fCache->getRelativeDateTimeUnitFormatter(fStyle, unit, bFuture, pluralIndex); + fCache->getRelativeDateTimeUnitFormatter(fStyle, unit, bFuture, pluralForm); if (formatter == nullptr) { // TODO: WARN - look at quantity formatter's action with an error. status = U_INVALID_FORMAT_ERROR; - return appendTo; + return; } - formatter->format(formattedNumber, result, status); - adjustForContext(result); - return appendTo.append(result); + + number::impl::SimpleModifier modifier(*formatter, kRDTLiteralField, false); + modifier.formatAsPrefixSuffix( + output.getStringRef(), 0, output.getStringRef().length(), status); } UnicodeString& RelativeDateTimeFormatter::format( - UDateDirection direction, UDateAbsoluteUnit unit, - UnicodeString& appendTo, UErrorCode& status) const { + UDateDirection direction, + UDateAbsoluteUnit unit, + UnicodeString& appendTo, + UErrorCode& status) const { + return doFormat( + &RelativeDateTimeFormatter::formatAbsoluteImpl, + appendTo, + status, + direction, + unit); +} + +FormattedRelativeDateTime RelativeDateTimeFormatter::formatToValue( + UDateDirection direction, + UDateAbsoluteUnit unit, + UErrorCode& status) const { + return doFormatToValue( + &RelativeDateTimeFormatter::formatAbsoluteImpl, + status, + direction, + unit); +} + +void RelativeDateTimeFormatter::formatAbsoluteImpl( + UDateDirection direction, + UDateAbsoluteUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const { if (U_FAILURE(status)) { - return appendTo; + return; } if (unit == UDAT_ABSOLUTE_NOW && direction != UDAT_DIRECTION_PLAIN) { status = U_ILLEGAL_ARGUMENT_ERROR; - return appendTo; + return; } // Get string using fallback. - UnicodeString result; - result.fastCopyFrom(fCache->getAbsoluteUnitString(fStyle, unit, direction)); - if (fOptBreakIterator != nullptr) { - adjustForContext(result); - } - return appendTo.append(result); + output.getStringRef().append( + fCache->getAbsoluteUnitString(fStyle, unit, direction), + kRDTLiteralField, + status); } UnicodeString& RelativeDateTimeFormatter::format( - double offset, URelativeDateTimeUnit unit, - UnicodeString& appendTo, UErrorCode& status) const { + double offset, + URelativeDateTimeUnit unit, + UnicodeString& appendTo, + UErrorCode& status) const { + return doFormat( + &RelativeDateTimeFormatter::formatRelativeImpl, + appendTo, + status, + offset, + unit); +} + +FormattedRelativeDateTime RelativeDateTimeFormatter::formatToValue( + double offset, + URelativeDateTimeUnit unit, + UErrorCode& status) const { + return doFormatToValue( + &RelativeDateTimeFormatter::formatRelativeImpl, + status, + offset, + unit); +} + +void RelativeDateTimeFormatter::formatRelativeImpl( + double offset, + URelativeDateTimeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const { if (U_FAILURE(status)) { - return appendTo; + return; } // TODO: // The full implementation of this depends on CLDR data that is not yet available, @@ -981,20 +1162,13 @@ UnicodeString& RelativeDateTimeFormatter::format( default: break; } if (direction != UDAT_DIRECTION_COUNT && absunit != UDAT_ABSOLUTE_UNIT_COUNT) { - const UnicodeString &unitFormatString = - fCache->getAbsoluteUnitString(fStyle, absunit, direction); - if (!unitFormatString.isEmpty()) { - if (fOptBreakIterator != nullptr) { - UnicodeString result(unitFormatString); - adjustForContext(result); - return appendTo.append(result); - } else { - return appendTo.append(unitFormatString); - } + formatAbsoluteImpl(direction, absunit, output, status); + if (output.getStringRef().length() != 0) { + return; } } // otherwise fallback to formatNumeric - return formatNumeric(offset, unit, appendTo, status); + formatNumericImpl(offset, unit, output, status); } UnicodeString& RelativeDateTimeFormatter::combineDateAndTime( @@ -1004,10 +1178,10 @@ UnicodeString& RelativeDateTimeFormatter::combineDateAndTime( timeString, relativeDateString, appendTo, status); } -void RelativeDateTimeFormatter::adjustForContext(UnicodeString &str) const { +UnicodeString& RelativeDateTimeFormatter::adjustForContext(UnicodeString &str) const { if (fOptBreakIterator == nullptr || str.length() == 0 || !u_islower(str.char32At(0))) { - return; + return str; } // Must guarantee that one thread at a time accesses the shared break @@ -1017,6 +1191,17 @@ void RelativeDateTimeFormatter::adjustForContext(UnicodeString &str) const { fOptBreakIterator->get(), fLocale, U_TITLECASE_NO_LOWERCASE | U_TITLECASE_NO_BREAK_ADJUSTMENT); + return str; +} + +UBool RelativeDateTimeFormatter::checkNoAdjustForContext(UErrorCode& status) const { + // This is unsupported because it's hard to keep fields in sync with title + // casing. The code could be written and tested if there is demand. + if (fOptBreakIterator != nullptr) { + status = U_UNSUPPORTED_ERROR; + return FALSE; + } + return TRUE; } void RelativeDateTimeFormatter::init( @@ -1072,6 +1257,17 @@ U_NAMESPACE_END U_NAMESPACE_USE + +// Magic number: "FRDT" (FormattedRelativeDateTime) in ASCII +UPRV_FORMATTED_VALUE_CAPI_AUTO_IMPL( + FormattedRelativeDateTime, + UFormattedRelativeDateTime, + UFormattedRelativeDateTimeImpl, + UFormattedRelativeDateTimeApiHelper, + ureldatefmt, + 0x46524454) + + U_CAPI URelativeDateTimeFormatter* U_EXPORT2 ureldatefmt_open( const char* locale, UNumberFormat* nfToAdopt, @@ -1125,6 +1321,21 @@ ureldatefmt_formatNumeric( const URelativeDateTimeFormatter* reldatefmt, return res.extract(result, resultCapacity, *status); } +U_STABLE void U_EXPORT2 +ureldatefmt_formatNumericToResult( + const URelativeDateTimeFormatter* reldatefmt, + double offset, + URelativeDateTimeUnit unit, + UFormattedRelativeDateTime* result, + UErrorCode* status) { + if (U_FAILURE(*status)) { + return; + } + auto* fmt = reinterpret_cast(reldatefmt); + auto* resultImpl = UFormattedRelativeDateTimeApiHelper::validate(result, *status); + resultImpl->fImpl = fmt->formatNumericToValue(offset, unit, *status); +} + U_CAPI int32_t U_EXPORT2 ureldatefmt_format( const URelativeDateTimeFormatter* reldatefmt, double offset, @@ -1153,6 +1364,21 @@ ureldatefmt_format( const URelativeDateTimeFormatter* reldatefmt, return res.extract(result, resultCapacity, *status); } +U_DRAFT void U_EXPORT2 +ureldatefmt_formatToResult( + const URelativeDateTimeFormatter* reldatefmt, + double offset, + URelativeDateTimeUnit unit, + UFormattedRelativeDateTime* result, + UErrorCode* status) { + if (U_FAILURE(*status)) { + return; + } + auto* fmt = reinterpret_cast(reldatefmt); + auto* resultImpl = UFormattedRelativeDateTimeApiHelper::validate(result, *status); + resultImpl->fImpl = fmt->formatToValue(offset, unit, *status); +} + U_CAPI int32_t U_EXPORT2 ureldatefmt_combineDateAndTime( const URelativeDateTimeFormatter* reldatefmt, const UChar * relativeDateString, diff --git a/icu4c/source/i18n/unicode/formattedvalue.h b/icu4c/source/i18n/unicode/formattedvalue.h index 3f195fb623d..457f119111b 100644 --- a/icu4c/source/i18n/unicode/formattedvalue.h +++ b/icu4c/source/i18n/unicode/formattedvalue.h @@ -222,7 +222,7 @@ class U_I18N_API ConstrainedFieldPosition : public UMemory { int32_t limit); /** @internal */ - UBool matchesField(UFieldCategory category, int32_t field); + UBool matchesField(int32_t category, int32_t field); private: int64_t fContext = 0LL; diff --git a/icu4c/source/i18n/unicode/reldatefmt.h b/icu4c/source/i18n/unicode/reldatefmt.h index be06b1013d5..995fa2dbaee 100644 --- a/icu4c/source/i18n/unicode/reldatefmt.h +++ b/icu4c/source/i18n/unicode/reldatefmt.h @@ -19,6 +19,7 @@ #include "unicode/udisplaycontext.h" #include "unicode/ureldatefmt.h" #include "unicode/locid.h" +#include "unicode/formattedvalue.h" /** * \file @@ -245,6 +246,70 @@ class SharedPluralRules; class SharedBreakIterator; class NumberFormat; class UnicodeString; +class FormattedRelativeDateTimeData; + + +/** + * An immutable class containing the result of a relative datetime formatting operation. + * + * Not intended for public subclassing. + * + * @draft ICU 64 + */ +class U_I18N_API FormattedRelativeDateTime : public UMemory, public FormattedValue { + public: + /** + * Default constructor; makes an empty FormattedRelativeDateTime. + * @draft ICU 64 + */ + FormattedRelativeDateTime() : fData(nullptr), fErrorCode(U_INVALID_STATE_ERROR) {}; + + /** + * Move constructor: Leaves the source FormattedRelativeDateTime in an undefined state. + * @draft ICU 64 + */ + FormattedRelativeDateTime(FormattedRelativeDateTime&& src) U_NOEXCEPT; + + /** + * Destruct an instance of FormattedRelativeDateTime. + * @draft ICU 64 + */ + virtual ~FormattedRelativeDateTime() U_OVERRIDE; + + /** Copying not supported; use move constructor instead. */ + FormattedRelativeDateTime(const FormattedRelativeDateTime&) = delete; + + /** Copying not supported; use move assignment instead. */ + FormattedRelativeDateTime& operator=(const FormattedRelativeDateTime&) = delete; + + /** + * Move assignment: Leaves the source FormattedRelativeDateTime in an undefined state. + * @draft ICU 64 + */ + FormattedRelativeDateTime& operator=(FormattedRelativeDateTime&& src) U_NOEXCEPT; + + /** @copydoc FormattedValue::toString() */ + UnicodeString toString(UErrorCode& status) const U_OVERRIDE; + + /** @copydoc FormattedValue::toTempString() */ + 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; + + private: + FormattedRelativeDateTimeData *fData; + UErrorCode fErrorCode; + explicit FormattedRelativeDateTime(FormattedRelativeDateTimeData *results) + : fData(results), fErrorCode(U_ZERO_ERROR) {}; + explicit FormattedRelativeDateTime(UErrorCode errorCode) + : fData(nullptr), fErrorCode(errorCode) {}; + friend class RelativeDateTimeFormatter; +}; + /** * Formats simple relative dates. There are two types of relative dates that @@ -386,6 +451,10 @@ public: /** * Formats a relative date with a quantity such as "in 5 days" or * "3 months ago" + * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param quantity The numerical amount e.g 5. This value is formatted * according to this object's NumberFormat object. * @param direction NEXT means a future relative date; LAST means a past @@ -405,8 +474,35 @@ public: UnicodeString& appendTo, UErrorCode& status) const; + /** + * Formats a relative date with a quantity such as "in 5 days" or + * "3 months ago" + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * @param quantity The numerical amount e.g 5. This value is formatted + * according to this object's NumberFormat object. + * @param direction NEXT means a future relative date; LAST means a past + * relative date. If direction is anything else, this method sets + * status to U_ILLEGAL_ARGUMENT_ERROR. + * @param unit the unit e.g day? month? year? + * @param status ICU error code returned here. + * @return The formatted relative datetime + * @draft ICU 64 + */ + FormattedRelativeDateTime formatToValue( + double quantity, + UDateDirection direction, + UDateRelativeUnit unit, + UErrorCode& status) const; + /** * Formats a relative date without a quantity. + * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param direction NEXT, LAST, THIS, etc. * @param unit e.g SATURDAY, DAY, MONTH * @param appendTo The string to which the formatted result will be @@ -423,10 +519,33 @@ public: UnicodeString& appendTo, UErrorCode& status) const; + /** + * Formats a relative date without a quantity. + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * If the string is not available in the requested locale, the return + * value will be empty (calling toString will give an empty string). + * + * @param direction NEXT, LAST, THIS, etc. + * @param unit e.g SATURDAY, DAY, MONTH + * @param status ICU error code returned here. + * @return The formatted relative datetime + * @draft ICU 64 + */ + FormattedRelativeDateTime formatToValue( + UDateDirection direction, + UDateAbsoluteUnit unit, + UErrorCode& status) const; + /** * Format a combination of URelativeDateTimeUnit and numeric offset * using a numeric style, e.g. "1 week ago", "in 1 week", * "5 weeks ago", "in 5 weeks". + * + * This method returns a String. To get more information about the + * formatting result, use formatNumericToValue(). * * @param offset The signed offset for the specified unit. This * will be formatted according to this object's @@ -446,6 +565,29 @@ public: UnicodeString& appendTo, UErrorCode& status) const; + /** + * Format a combination of URelativeDateTimeUnit and numeric offset + * using a numeric style, e.g. "1 week ago", "in 1 week", + * "5 weeks ago", "in 5 weeks". + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by formatNumeric(). + * + * @param offset The signed offset for the specified unit. This + * will be formatted according to this object's + * NumberFormat object. + * @param unit The unit to use when formatting the relative + * date, e.g. UDAT_REL_UNIT_WEEK, + * UDAT_REL_UNIT_FRIDAY. + * @param status ICU error code returned here. + * @return The formatted relative datetime + * @draft ICU 64 + */ + FormattedRelativeDateTime formatNumericToValue( + double offset, + URelativeDateTimeUnit unit, + UErrorCode& status) const; + /** * Format a combination of URelativeDateTimeUnit and numeric offset * using a text style if possible, e.g. "last week", "this week", @@ -453,6 +595,9 @@ public: * style if no appropriate text term is available for the specified * offset in the object's locale. * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param offset The signed offset for the specified unit. * @param unit The unit to use when formatting the relative * date, e.g. UDAT_REL_UNIT_WEEK, @@ -469,6 +614,29 @@ public: UnicodeString& appendTo, UErrorCode& status) const; + /** + * Format a combination of URelativeDateTimeUnit and numeric offset + * using a text style if possible, e.g. "last week", "this week", + * "next week", "yesterday", "tomorrow". Falls back to numeric + * style if no appropriate text term is available for the specified + * offset in the object's locale. + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * @param offset The signed offset for the specified unit. + * @param unit The unit to use when formatting the relative + * date, e.g. UDAT_REL_UNIT_WEEK, + * UDAT_REL_UNIT_FRIDAY. + * @param status ICU error code returned here. + * @return The formatted relative datetime + * @draft ICU 64 + */ + FormattedRelativeDateTime formatToValue( + double offset, + URelativeDateTimeUnit unit, + UErrorCode& status) const; + /** * Combines a relative date string and a time string in this object's * locale. This is done with the same date-time separator used for the @@ -520,7 +688,43 @@ private: NumberFormat *nfToAdopt, BreakIterator *brkIter, UErrorCode &status); - void adjustForContext(UnicodeString &) const; + UnicodeString& adjustForContext(UnicodeString &) const; + UBool checkNoAdjustForContext(UErrorCode& status) const; + + template + UnicodeString& doFormat( + F callback, + UnicodeString& appendTo, + UErrorCode& status, + Args... args) const; + + template + FormattedRelativeDateTime doFormatToValue( + F callback, + UErrorCode& status, + Args... args) const; + + void formatImpl( + double quantity, + UDateDirection direction, + UDateRelativeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const; + void formatAbsoluteImpl( + UDateDirection direction, + UDateAbsoluteUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const; + void formatNumericImpl( + double offset, + URelativeDateTimeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const; + void formatRelativeImpl( + double offset, + URelativeDateTimeUnit unit, + FormattedRelativeDateTimeData& output, + UErrorCode& status) const; }; U_NAMESPACE_END diff --git a/icu4c/source/i18n/unicode/uformattedvalue.h b/icu4c/source/i18n/unicode/uformattedvalue.h index e4d00026127..4dff30f20d8 100644 --- a/icu4c/source/i18n/unicode/uformattedvalue.h +++ b/icu4c/source/i18n/unicode/uformattedvalue.h @@ -60,6 +60,13 @@ typedef enum UFieldCategory { */ UFIELD_CATEGORY_LIST, + /** + * For fields in URelativeDateTimeFormatterField (ureldatefmt.h), from ICU 64. + * + * @draft ICU 64 + */ + UFIELD_CATEGORY_RELATIVE_DATETIME, + #ifndef U_HIDE_INTERNAL_API /** @internal */ UFIELD_CATEGORY_COUNT diff --git a/icu4c/source/i18n/unicode/unumberformatter.h b/icu4c/source/i18n/unicode/unumberformatter.h index 96bbbd263dc..7b520cbff81 100644 --- a/icu4c/source/i18n/unicode/unumberformatter.h +++ b/icu4c/source/i18n/unicode/unumberformatter.h @@ -551,15 +551,17 @@ unumf_formatDecimal(const UNumberFormatter* uformatter, const char* value, int32 /** - * Returns a representation of a UFormattedNumber as a UFormattedValue, which can be - * subsequently passed to any API requiring that type. + * 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. + * 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. + * You can think of this method as a cast between types. + * + * @param uresult The object containing the formatted string. * @param ec Set if an error occurs. - * @return A representation of the given UFormattedNumber as a UFormattedValue. + * @return A UFormattedValue owned by the input object. * @draft ICU 64 */ U_DRAFT const UFormattedValue* U_EXPORT2 diff --git a/icu4c/source/i18n/unicode/ureldatefmt.h b/icu4c/source/i18n/unicode/ureldatefmt.h index 0fde188d0f4..9140c551b21 100644 --- a/icu4c/source/i18n/unicode/ureldatefmt.h +++ b/icu4c/source/i18n/unicode/ureldatefmt.h @@ -17,6 +17,7 @@ #include "unicode/unum.h" #include "unicode/udisplaycontext.h" #include "unicode/localpointer.h" +#include "unicode/uformattedvalue.h" /** * \file @@ -174,6 +175,27 @@ typedef enum URelativeDateTimeUnit { #endif /* U_HIDE_DEPRECATED_API */ } URelativeDateTimeUnit; +#ifndef U_HIDE_DRAFT_API +/** + * FieldPosition and UFieldPosition selectors for format fields + * defined by RelativeDateTimeFormatter. + * @draft ICU 64 + */ +typedef enum URelativeDateTimeFormatterField { + /** + * Represents a literal text string, like "tomorrow" or "days ago". + * @draft ICU 64 + */ + UDAT_REL_LITERAL_FIELD, + /** + * Represents a number quantity, like "3" in "3 days ago". + * @draft ICU 64 + */ + UDAT_REL_NUMERIC_FIELD, +} URelativeDateTimeFormatterField; +#endif // U_HIDE_DRAFT_API + + /** * Opaque URelativeDateTimeFormatter object for use in C programs. * @stable ICU 57 @@ -230,6 +252,53 @@ ureldatefmt_open( const char* locale, U_STABLE void U_EXPORT2 ureldatefmt_close(URelativeDateTimeFormatter *reldatefmt); + +struct UFormattedRelativeDateTime; +/** + * Opaque struct to contain the results of a URelativeDateTimeFormatter operation. + * @draft ICU 64 + */ +typedef struct UFormattedRelativeDateTime UFormattedRelativeDateTime; + +/** + * Creates an object to hold the result of a URelativeDateTimeFormatter + * operation. The object can be used repeatedly; it is cleared whenever + * passed to a format function. + * + * @param ec Set if an error occurs. + * @return A pointer needing ownership. + * @draft ICU 64 + */ +U_DRAFT UFormattedRelativeDateTime* U_EXPORT2 +ureldatefmt_openResult(UErrorCode* ec); + +/** + * Returns a representation of a UFormattedRelativeDateTime as a UFormattedValue, + * which can be subsequently passed to any API requiring that type. + * + * The returned object is owned by the UFormattedRelativeDateTime and is valid + * only as long as the UFormattedRelativeDateTime is present and unchanged in memory. + * + * You can think of this method as a cast between types. + * + * @param ufrdt The object containing the formatted string. + * @param ec Set if an error occurs. + * @return A UFormattedValue owned by the input object. + * @draft ICU 64 + */ +U_DRAFT const UFormattedValue* U_EXPORT2 +ureldatefmt_resultAsValue(const UFormattedRelativeDateTime* ufrdt, UErrorCode* ec); + +/** + * Releases the UFormattedRelativeDateTime created by ureldatefmt_openResult. + * + * @param ufrdt The object to release. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ureldatefmt_closeResult(UFormattedRelativeDateTime* ufrdt); + + #if U_SHOW_CPLUSPLUS_API U_NAMESPACE_BEGIN @@ -245,6 +314,17 @@ U_NAMESPACE_BEGIN */ U_DEFINE_LOCAL_OPEN_POINTER(LocalURelativeDateTimeFormatterPointer, URelativeDateTimeFormatter, ureldatefmt_close); +/** + * \class LocalUFormattedRelativeDateTimePointer + * "Smart pointer" class, closes a UFormattedRelativeDateTime via ureldatefmt_closeResult(). + * For most methods see the LocalPointerBase base class. + * + * @see LocalPointerBase + * @see LocalPointer + * @draft ICU 64 + */ +U_DEFINE_LOCAL_OPEN_POINTER(LocalUFormattedRelativeDateTimePointer, UFormattedRelativeDateTime, ureldatefmt_closeResult); + U_NAMESPACE_END #endif @@ -285,6 +365,37 @@ ureldatefmt_formatNumeric( const URelativeDateTimeFormatter* reldatefmt, int32_t resultCapacity, UErrorCode* status); +/** + * Format a combination of URelativeDateTimeUnit and numeric + * offset using a numeric style, e.g. "1 week ago", "in 1 week", + * "5 weeks ago", "in 5 weeks". + * + * @param reldatefmt + * The URelativeDateTimeFormatter object specifying the + * format conventions. + * @param offset + * The signed offset for the specified unit. This will + * be formatted according to this object's UNumberFormat + * object. + * @param unit + * The unit to use when formatting the relative + * date, e.g. UDAT_REL_UNIT_WEEK, UDAT_REL_UNIT_FRIDAY. + * @param result + * A pointer to a UFormattedRelativeDateTime to populate. + * @param status + * A pointer to a UErrorCode to receive any errors. In + * case of error status, the contents of result are + * undefined. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ureldatefmt_formatNumericToResult( + const URelativeDateTimeFormatter* reldatefmt, + double offset, + URelativeDateTimeUnit unit, + UFormattedRelativeDateTime* result, + UErrorCode* status); + /** * Format a combination of URelativeDateTimeUnit and numeric offset * using a text style if possible, e.g. "last week", "this week", @@ -321,6 +432,40 @@ ureldatefmt_format( const URelativeDateTimeFormatter* reldatefmt, int32_t resultCapacity, UErrorCode* status); +/** + * Format a combination of URelativeDateTimeUnit and numeric offset + * using a text style if possible, e.g. "last week", "this week", + * "next week", "yesterday", "tomorrow". Falls back to numeric + * style if no appropriate text term is available for the specified + * offset in the object's locale. + * + * This method populates a UFormattedRelativeDateTime, which exposes more + * information than the string populated by format(). + * + * @param reldatefmt + * The URelativeDateTimeFormatter object specifying the + * format conventions. + * @param offset + * The signed offset for the specified unit. + * @param unit + * The unit to use when formatting the relative + * date, e.g. UDAT_REL_UNIT_WEEK, UDAT_REL_UNIT_FRIDAY. + * @param result + * A pointer to a UFormattedRelativeDateTime to populate. + * @param status + * A pointer to a UErrorCode to receive any errors. In + * case of error status, the contents of result are + * undefined. + * @draft ICU 64 + */ +U_DRAFT void U_EXPORT2 +ureldatefmt_formatToResult( + const URelativeDateTimeFormatter* reldatefmt, + double offset, + URelativeDateTimeUnit unit, + UFormattedRelativeDateTime* result, + UErrorCode* status); + /** * Combines a relative date string and a time string in this object's * locale. This is done with the same date-time separator used for the diff --git a/icu4c/source/test/cintltst/crelativedateformattest.c b/icu4c/source/test/cintltst/crelativedateformattest.c index 13fe42eaddc..46c98a7d68a 100644 --- a/icu4c/source/test/cintltst/crelativedateformattest.c +++ b/icu4c/source/test/cintltst/crelativedateformattest.c @@ -16,9 +16,12 @@ #include "unicode/ustring.h" #include "cintltst.h" #include "cmemory.h" +#include "cformtst.h" static void TestRelDateFmt(void); +static void TestNumericField(void); static void TestCombineDateTime(void); +static void TestFields(void); void addRelativeDateFormatTest(TestNode** root); @@ -27,12 +30,20 @@ void addRelativeDateFormatTest(TestNode** root); void addRelativeDateFormatTest(TestNode** root) { TESTCASE(TestRelDateFmt); + TESTCASE(TestNumericField); TESTCASE(TestCombineDateTime); + TESTCASE(TestFields); } static const double offsets[] = { -5.0, -2.2, -2.0, -1.0, -0.7, -0.0, 0.0, 0.7, 1.0, 2.0, 5.0 }; enum { kNumOffsets = UPRV_LENGTHOF(offsets) }; +typedef struct { + int32_t field; + int32_t beginPos; + int32_t endPos; +} FieldsDat; + static const char* en_decDef_long_midSent_sec[kNumOffsets*2] = { /* text numeric */ "5 seconds ago", "5 seconds ago", /* -5 */ @@ -48,6 +59,21 @@ static const char* en_decDef_long_midSent_sec[kNumOffsets*2] = { "in 5 seconds", "in 5 seconds" /* 5 */ }; +static const FieldsDat en_attrDef_long_midSent_sec[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 seconds ago", "5 seconds ago", -5 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "2.2 seconds ago", "2.2 seconds ago", -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 seconds ago", "2 seconds ago", -2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "1 second ago", "1 second ago", -1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "0.7 seconds ago", "0.7 seconds ago", -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "now", "0 seconds ago", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "now", "in 0 seconds", 0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 6}, {UDAT_REL_NUMERIC_FIELD, 3, 6}, /* "in 0.7 seconds", "in 0.7 seconds", 0.7 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 1 second", "in 1 second", 1 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 seconds", "in 2 seconds", 2 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 seconds", "in 5 seconds" 5 */ +}; + static const char* en_decDef_long_midSent_week[kNumOffsets*2] = { /* text numeric */ "5 weeks ago", "5 weeks ago", /* -5 */ @@ -63,6 +89,21 @@ static const char* en_decDef_long_midSent_week[kNumOffsets*2] = { "in 5 weeks", "in 5 weeks" /* 5 */ }; +static const FieldsDat en_attrDef_long_midSent_week[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 weeks ago", "5 weeks ago", -5 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "2.2 weeks ago", "2.2 weeks ago", -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 weeks ago", "2 weeks ago", -2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "last week", "1 week ago", -1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "0.7 weeks ago", "0.7 weeks ago", -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "this week", "0 weeks ago", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "this week", "in 0 weeks", 0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 6}, {UDAT_REL_NUMERIC_FIELD, 3, 6}, /* "in 0.7 weeks", "in 0.7 weeks", 0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "next week", "in 1 week", 1 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 weeks", "in 2 weeks", 2 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 weeks", "in 5 weeks" 5 */ +}; + static const char* en_dec0_long_midSent_week[kNumOffsets*2] = { /* text numeric */ "5 weeks ago", "5 weeks ago", /* -5 */ @@ -78,6 +119,21 @@ static const char* en_dec0_long_midSent_week[kNumOffsets*2] = { "in 5 weeks", "in 5 weeks" /* 5 */ }; +static const FieldsDat en_attr0_long_midSent_week[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 weeks ago", "5 weeks ago", -5 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 weeks ago", "2 weeks ago", -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 weeks ago", "2 weeks ago", -2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "last week", "1 week ago", -1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "0 weeks ago", "0 weeks ago", -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "this week", "0 weeks ago", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "this week", "in 0 weeks", 0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 0 weeks", "in 0 weeks", 0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "next week", "in 1 week", 1 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 weeks", "in 2 weeks", 2 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 weeks", "in 5 weeks" 5 */ +}; + static const char* en_decDef_short_midSent_week[kNumOffsets*2] = { /* text numeric */ "5 wk. ago", "5 wk. ago", /* -5 */ @@ -93,6 +149,21 @@ static const char* en_decDef_short_midSent_week[kNumOffsets*2] = { "in 5 wk.", "in 5 wk." /* 5 */ }; +static const FieldsDat en_attrDef_short_midSent_week[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 wk. ago", "5 wk. ago", -5 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "2.2 wk. ago", "2.2 wk. ago", -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 wk. ago", "2 wk. ago", -2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "last wk.", "1 wk. ago", -1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "0.7 wk. ago", "0.7 wk. ago", -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "this wk.", "0 wk. ago", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "this wk.", "in 0 wk.", 0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 6}, {UDAT_REL_NUMERIC_FIELD, 3, 6}, /* "in 0.7 wk.", "in 0.7 wk.", 0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "next wk.", "in 1 wk.", 1 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 wk.", "in 2 wk.", 2 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 wk.", "in 5 wk." 5 */ +}; + static const char* en_decDef_long_midSent_min[kNumOffsets*2] = { /* text numeric */ "5 minutes ago", "5 minutes ago", /* -5 */ @@ -108,6 +179,21 @@ static const char* en_decDef_long_midSent_min[kNumOffsets*2] = { "in 5 minutes", "in 5 minutes" /* 5 */ }; +static const FieldsDat en_attrDef_long_midSent_min[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 minutes ago", "5 minutes ago", -5 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "2.2 minutes ago", "2.2 minutes ago", -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 minutes ago", "2 minutes ago", -2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "1 minute ago", "1 minute ago", -1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, /* "0.7 minutes ago", "0.7 minutes ago", -0.7 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "0 minutes ago", "0 minutes ago", -0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 0 minutes", "in 0 minutes", 0 */ + {UDAT_REL_NUMERIC_FIELD, 3, 6}, {UDAT_REL_NUMERIC_FIELD, 3, 6}, /* "in 0.7 minutes", "in 0.7 minutes", 0.7 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 1 minute", "in 1 minute", 1 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 minutes", "in 2 minutes", 2 */ + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 minutes", "in 5 minutes" 5 */ +}; + static const char* en_dec0_long_midSent_tues[kNumOffsets*2] = { /* text numeric */ "5 Tuesdays ago", "5 Tuesdays ago", /* -5 */ @@ -123,6 +209,21 @@ static const char* en_dec0_long_midSent_tues[kNumOffsets*2] = { "in 5 Tuesdays", "in 5 Tuesdays", /* 5 */ }; +static const FieldsDat en_attr0_long_midSent_tues[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "5 Tuesdays ago", "5 Tuesdays ago", -5 */ + { -1, -1, -1}, { -1, -1, -1}, /* "" , "" , -2.2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "2 Tuesdays ago", "2 Tuesdays ago", -2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "last Tuesday", "1 Tuesday ago", -1 */ + { -1, -1, -1}, { -1, -1, -1}, /* "" , "" , -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, /* "this Tuesday", "0 Tuesdays ago", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "this Tuesday", "in 0 Tuesdays", 0 */ + { -1, -1, -1}, { -1, -1, -1}, /* "" , "" , 0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "next Tuesday", "in 1 Tuesday", 1 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 2 Tuesdays", "in 2 Tuesdays", 2 */ + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, /* "in 5 Tuesdays", "in 5 Tuesdays", 5 */ +}; + static const char* fr_decDef_long_midSent_day[kNumOffsets*2] = { /* text numeric */ "il y a 5 jours", "il y a 5 jours", /* -5 */ @@ -138,6 +239,21 @@ static const char* fr_decDef_long_midSent_day[kNumOffsets*2] = { "dans 5 jours", "dans 5 jours" /* 5 */ }; +static const FieldsDat fr_attrDef_long_midSent_day[kNumOffsets*2] = { +/* text numeric text numeric */ + {UDAT_REL_NUMERIC_FIELD, 7, 8}, {UDAT_REL_NUMERIC_FIELD, 7, 8}, /* "il y a 5 jours", "il y a 5 jours", -5 */ + {UDAT_REL_NUMERIC_FIELD, 7, 10}, {UDAT_REL_NUMERIC_FIELD, 7, 10}, /* "il y a 2,2 jours", "il y a 2,2 jours", -2.2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 7, 8}, /* "avant-hier", "il y a 2 jours", -2 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 7, 8}, /* "hier", "il y a 1 jour", -1 */ + {UDAT_REL_NUMERIC_FIELD, 7, 10}, {UDAT_REL_NUMERIC_FIELD, 7, 10}, /* "il y a 0,7 jour", "il y a 0,7 jour", -0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 7, 8}, /* "aujourd\\u2019hui", "il y a 0 jour", -0 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 5, 6}, /* "aujourd\\u2019hui", "dans 0 jour", 0 */ + {UDAT_REL_NUMERIC_FIELD, 5, 8}, {UDAT_REL_NUMERIC_FIELD, 5, 8}, /* "dans 0,7 jour", "dans 0,7 jour", 0.7 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 5, 6}, /* "demain", "dans 1 jour", 1 */ + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 5, 6}, /* "apr\\u00E8s-demain", "dans 2 jours", 2 */ + {UDAT_REL_NUMERIC_FIELD, 5, 6}, {UDAT_REL_NUMERIC_FIELD, 5, 6}, /* "dans 5 jours", "dans 5 jours" 5 */ +}; + static const char* ak_decDef_long_stdAlon_sec[kNumOffsets*2] = { // falls back to root /* text numeric */ "-5 s", "-5 s", /* -5 */ @@ -153,6 +269,20 @@ static const char* ak_decDef_long_stdAlon_sec[kNumOffsets*2] = { // falls back t "+5 s", "+5 s", /* 5 */ }; +static const FieldsDat ak_attrDef_long_stdAlon_sec[kNumOffsets*2] = { + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 4}, {UDAT_REL_NUMERIC_FIELD, 1, 4}, + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 4}, {UDAT_REL_NUMERIC_FIELD, 1, 4}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 4}, {UDAT_REL_NUMERIC_FIELD, 1, 4}, + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, + {UDAT_REL_NUMERIC_FIELD, 1, 2}, {UDAT_REL_NUMERIC_FIELD, 1, 2}, +}; + static const char* enIN_decDef_short_midSent_weds[kNumOffsets*2] = { /* text numeric */ "5 Wed. ago", "5 Wed. ago", /* -5 */ @@ -168,6 +298,20 @@ static const char* enIN_decDef_short_midSent_weds[kNumOffsets*2] = { "in 5 Wed.", "in 5 Wed." /* 5 */ }; +static const FieldsDat enIN_attrDef_short_midSent_weds[kNumOffsets*2] = { + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, + {UDAT_REL_NUMERIC_FIELD, 0, 1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, + {UDAT_REL_NUMERIC_FIELD, 0, 3}, {UDAT_REL_NUMERIC_FIELD, 0, 3}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 0, 1}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, + {UDAT_REL_NUMERIC_FIELD, 3, 6}, {UDAT_REL_NUMERIC_FIELD, 3, 6}, + { -1, -1, -1}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, + {UDAT_REL_NUMERIC_FIELD, 3, 4}, {UDAT_REL_NUMERIC_FIELD, 3, 4}, +}; + typedef struct { const char* locale; int32_t decPlaces; /* fixed decimal places; -1 to use default num formatter */ @@ -175,19 +319,29 @@ typedef struct { UDisplayContext capContext; URelativeDateTimeUnit unit; const char ** expectedResults; /* for the various offsets */ + const FieldsDat* expectedAttributes; } RelDateTimeFormatTestItem; static const RelDateTimeFormatTestItem fmtTestItems[] = { - { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_SECOND, en_decDef_long_midSent_sec }, - { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, en_decDef_long_midSent_week }, - { "en", 0, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, en_dec0_long_midSent_week }, - { "en", -1, UDAT_STYLE_SHORT, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, en_decDef_short_midSent_week }, - { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_MINUTE, en_decDef_long_midSent_min }, - { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_TUESDAY, en_dec0_long_midSent_tues }, - { "fr", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_DAY, fr_decDef_long_midSent_day }, - { "ak", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_STANDALONE, UDAT_REL_UNIT_SECOND, ak_decDef_long_stdAlon_sec }, - { "en_IN", -1, UDAT_STYLE_SHORT, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEDNESDAY, enIN_decDef_short_midSent_weds }, - { NULL, 0, (UDateRelativeDateTimeFormatterStyle)0, (UDisplayContext)0, (URelativeDateTimeUnit)0, NULL } /* terminator */ + { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_SECOND, + en_decDef_long_midSent_sec, en_attrDef_long_midSent_sec }, + { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, + en_decDef_long_midSent_week, en_attrDef_long_midSent_week}, + { "en", 0, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, + en_dec0_long_midSent_week, en_attr0_long_midSent_week}, + { "en", -1, UDAT_STYLE_SHORT, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEEK, + en_decDef_short_midSent_week, en_attrDef_short_midSent_week}, + { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_MINUTE, + en_decDef_long_midSent_min, en_attrDef_long_midSent_min}, + { "en", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_TUESDAY, + en_dec0_long_midSent_tues, en_attr0_long_midSent_tues}, + { "fr", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_DAY, + fr_decDef_long_midSent_day, fr_attrDef_long_midSent_day}, + { "ak", -1, UDAT_STYLE_LONG, UDISPCTX_CAPITALIZATION_FOR_STANDALONE, UDAT_REL_UNIT_SECOND, + ak_decDef_long_stdAlon_sec, ak_attrDef_long_stdAlon_sec}, + { "en_IN", -1, UDAT_STYLE_SHORT, UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, UDAT_REL_UNIT_WEDNESDAY, + enIN_decDef_short_midSent_weds, enIN_attrDef_short_midSent_weds}, + { NULL, 0, (UDateRelativeDateTimeFormatterStyle)0, (UDisplayContext)0, (URelativeDateTimeUnit)0, NULL, NULL } /* terminator */ }; enum { kUBufMax = 64, kBBufMax = 256 }; @@ -273,6 +427,154 @@ static void TestRelDateFmt() } } +static void TestNumericField() +{ + const RelDateTimeFormatTestItem *itemPtr; + log_verbose("\nTesting ureldatefmt_open(), ureldatefmt_formatForFields(), ureldatefmt_formatNumericForFields() with various parameters\n"); + for (itemPtr = fmtTestItems; itemPtr->locale != NULL; itemPtr++) { + URelativeDateTimeFormatter *reldatefmt = NULL; + UNumberFormat* nfToAdopt = NULL; + UErrorCode status = U_ZERO_ERROR; + int32_t iOffset; + + if (itemPtr->decPlaces >= 0) { + nfToAdopt = unum_open(UNUM_DECIMAL, NULL, 0, itemPtr->locale, NULL, &status); + if ( U_FAILURE(status) ) { + log_data_err("FAIL: unum_open(UNUM_DECIMAL, ...) for locale %s: %s\n", itemPtr->locale, myErrorName(status)); + continue; + } + unum_setAttribute(nfToAdopt, UNUM_MIN_FRACTION_DIGITS, itemPtr->decPlaces); + unum_setAttribute(nfToAdopt, UNUM_MAX_FRACTION_DIGITS, itemPtr->decPlaces); + unum_setAttribute(nfToAdopt, UNUM_ROUNDING_MODE, UNUM_ROUND_DOWN); + } + reldatefmt = ureldatefmt_open(itemPtr->locale, nfToAdopt, itemPtr->width, itemPtr->capContext, &status); + if ( U_FAILURE(status) ) { + log_data_err("FAIL: ureldatefmt_open() for locale %s, decPlaces %d, width %d, capContext %d: %s\n", + itemPtr->locale, itemPtr->decPlaces, (int)itemPtr->width, (int)itemPtr->capContext, + myErrorName(status) ); + continue; + } + + for (iOffset = 0; iOffset < kNumOffsets; iOffset++) { + if (itemPtr->unit >= UDAT_REL_UNIT_SUNDAY && offsets[iOffset] != -1.0 && offsets[iOffset] != 0.0 && offsets[iOffset] != 1.0) { + continue; /* we do not currently have data for this */ + } + + /* Depend on the next one to verify the data */ + status = U_ZERO_ERROR; + UFormattedRelativeDateTime* fv = ureldatefmt_openResult(&status); + if ( U_FAILURE(status) ) { + log_err("ureldatefmt_openResult fails, status %s\n", u_errorName(status)); + continue; + } + ureldatefmt_formatToResult(reldatefmt, offsets[iOffset], itemPtr->unit, fv, &status); + if ( U_FAILURE(status) ) { + log_err("FAIL: ureldatefmt_formatForFields() for locale %s, decPlaces %d, width %d, capContext %d, offset %.2f, unit %d: %s\n", + itemPtr->locale, itemPtr->decPlaces, (int)itemPtr->width, (int)itemPtr->capContext, + offsets[iOffset], (int)itemPtr->unit, myErrorName(status) ); + } else { + UChar ubufexp[kUBufMax]; + int32_t ulenexp = u_unescape(itemPtr->expectedResults[iOffset*2], ubufexp, kUBufMax); + int32_t ulenget; + const UChar* ubufget = ufmtval_getString(ureldatefmt_resultAsValue(fv, &status), &ulenget, &status); + assertUEquals("String content", ubufexp, ubufget); + assertIntEquals("String length", ulenexp, ulenget); + + FieldsDat expectedAttr = itemPtr->expectedAttributes[iOffset*2]; + UConstrainedFieldPosition* cfpos = ucfpos_open(&status); + UBool foundNumeric = FALSE; + while (TRUE) { + foundNumeric = ufmtval_nextPosition(ureldatefmt_resultAsValue(fv, &status), cfpos, &status); + if (!foundNumeric) { + break; + } + if (ucfpos_getCategory(cfpos, &status) == UFIELD_CATEGORY_RELATIVE_DATETIME + && ucfpos_getField(cfpos, &status) == UDAT_REL_NUMERIC_FIELD) { + break; + } + } + assertSuccess("Looking for numeric", &status); + int32_t beginPos, endPos; + ucfpos_getIndexes(cfpos, &beginPos, &endPos, &status); + if (expectedAttr.field == -1) { + if (foundNumeric) { + log_err("ureldatefmt_formatForFields as \"%s\"; expect no field, but got %d\n", + itemPtr->expectedResults[iOffset*2], + ucfpos_getField(cfpos, &status)); + } + } else { + if (!foundNumeric || + beginPos != expectedAttr.beginPos || + endPos != expectedAttr.endPos) { + log_err("ureldatefmt_formatForFields as \"%s\"; expect field %d range %d-%d, get range %d-%d\n", + itemPtr->expectedResults[iOffset*2], + expectedAttr.field, expectedAttr.beginPos, expectedAttr.endPos, + beginPos, endPos); + } + } + ucfpos_close(cfpos); + } + + if (itemPtr->unit >= UDAT_REL_UNIT_SUNDAY) { + ureldatefmt_closeResult(fv); + continue; /* we do not currently have numeric-style data for this */ + } + + /* Depend on the next one to verify the data */ + status = U_ZERO_ERROR; + ureldatefmt_formatNumericToResult(reldatefmt, offsets[iOffset], itemPtr->unit, fv, &status); + if ( U_FAILURE(status) ) { + log_err("FAIL: ureldatefmt_formatNumericForFields() for locale %s, decPlaces %d, width %d, capContext %d, offset %.2f, unit %d: %s\n", + itemPtr->locale, itemPtr->decPlaces, (int)itemPtr->width, (int)itemPtr->capContext, + offsets[iOffset], (int)itemPtr->unit, myErrorName(status) ); + } else { + UChar ubufexp[kUBufMax]; + int32_t ulenexp = u_unescape(itemPtr->expectedResults[iOffset*2 + 1], ubufexp, kUBufMax); + int32_t ulenget; + const UChar* ubufget = ufmtval_getString(ureldatefmt_resultAsValue(fv, &status), &ulenget, &status); + assertUEquals("String content", ubufexp, ubufget); + assertIntEquals("String length", ulenexp, ulenget); + + FieldsDat expectedAttr = itemPtr->expectedAttributes[iOffset*2 + 1]; + UConstrainedFieldPosition* cfpos = ucfpos_open(&status); + UBool foundNumeric = FALSE; + while (TRUE) { + foundNumeric = ufmtval_nextPosition(ureldatefmt_resultAsValue(fv, &status), cfpos, &status); + if (!foundNumeric) { + break; + } + if (ucfpos_getCategory(cfpos, &status) == UFIELD_CATEGORY_RELATIVE_DATETIME + && ucfpos_getField(cfpos, &status) == UDAT_REL_NUMERIC_FIELD) { + break; + } + } + assertSuccess("Looking for numeric", &status); + int32_t beginPos, endPos; + ucfpos_getIndexes(cfpos, &beginPos, &endPos, &status); + if (expectedAttr.field == -1) { + if (foundNumeric) { + log_err("ureldatefmt_formatForFields as \"%s\"; expect no field, but got %d rang %d-%d\n", + itemPtr->expectedResults[iOffset*2], + ucfpos_getField(cfpos, &status), beginPos, endPos); + } + } else { + if (!foundNumeric || + (beginPos != expectedAttr.beginPos || endPos != expectedAttr.endPos)) { + log_err("ureldatefmt_formatForFields as \"%s\"; expect field %d range %d-%d, get field %d range %d-%d\n", + itemPtr->expectedResults[iOffset*2 + 1], + expectedAttr.field, expectedAttr.beginPos, expectedAttr.endPos, + ucfpos_getField(cfpos, &status), beginPos, endPos); + } + } + ucfpos_close(cfpos); + } + ureldatefmt_closeResult(fv); + } + + ureldatefmt_close(reldatefmt); + } +} + typedef struct { const char* locale; UDateRelativeDateTimeFormatterStyle width; @@ -341,4 +643,54 @@ static void TestCombineDateTime() } } +static void TestFields() { + UErrorCode ec = U_ZERO_ERROR; + URelativeDateTimeFormatter* fmt = ureldatefmt_open( + "en-us", + NULL, + UDAT_STYLE_SHORT, + UDISPCTX_CAPITALIZATION_NONE, + &ec); + assertSuccess("Creating RelDTFmt", &ec); + UFormattedRelativeDateTime* frdt = ureldatefmt_openResult(&ec); + assertSuccess("Creating FmtVal", &ec); + + ureldatefmt_formatNumericToResult(fmt, -50, UDAT_REL_UNIT_SATURDAY, frdt, &ec); + assertSuccess("formatNumeric", &ec); + { + const UFormattedValue* fv = ureldatefmt_resultAsValue(frdt, &ec); + assertSuccess("Should convert without error", &ec); + static const UFieldPositionWithCategory expectedFieldPositions[] = { + // category, field, begin index, end index + {UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, 0, 2}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 0, 2}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 3, 11}}; + checkMixedFormattedValue( + "FormattedRelativeDateTime as FormattedValue (numeric)", + fv, + u"50 Sat. ago", + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + + ureldatefmt_formatToResult(fmt, -1, UDAT_REL_UNIT_WEEK, frdt, &ec); + assertSuccess("format", &ec); + { + const UFormattedValue* fv = ureldatefmt_resultAsValue(frdt, &ec); + assertSuccess("Should convert without error", &ec); + static const UFieldPositionWithCategory expectedFieldPositions[] = { + // category, field, begin index, end index + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 8}}; + checkMixedFormattedValue( + "FormattedRelativeDateTime as FormattedValue (relative)", + fv, + u"last wk.", + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + + ureldatefmt_closeResult(frdt); + ureldatefmt_close(fmt); +} + #endif /* #if !UCONFIG_NO_FORMATTING && !UCONFIG_NO_BREAK_ITERATION */ diff --git a/icu4c/source/test/depstest/dependencies.txt b/icu4c/source/test/depstest/dependencies.txt index 93c79193cef..d2682abf473 100644 --- a/icu4c/source/test/depstest/dependencies.txt +++ b/icu4c/source/test/depstest/dependencies.txt @@ -1005,7 +1005,7 @@ group: formatting # messageformat choicfmt.o msgfmt.o plurfmt.o selfmt.o umsg.o deps - decnumber formattable format units numberformatter numberparser + decnumber formattable format units numberformatter numberparser formatted_value_sbimpl listformatter dayperiodrules collation collation_builder # for rbnf @@ -1051,6 +1051,11 @@ group: formatted_value_iterimpl deps formatted_value format uvector32 +group: formatted_value_sbimpl + formattedval_sbimpl.o + deps + number_representation + group: format format.o fphdlimp.o fpositer.o ufieldpositer.o deps diff --git a/icu4c/source/test/intltest/numbertest_stringbuilder.cpp b/icu4c/source/test/intltest/numbertest_stringbuilder.cpp index 3106bedb310..eaf9a7a4d5f 100644 --- a/icu4c/source/test/intltest/numbertest_stringbuilder.cpp +++ b/icu4c/source/test/intltest/numbertest_stringbuilder.cpp @@ -184,8 +184,8 @@ void NumberStringBuilderTest::testFields() { assertSuccess("Appending to sb", status); assertEquals("Reference string copied twice", str.length() * 2, sb.length()); for (int32_t i = 0; i < str.length(); i++) { - assertEquals("Null field first", UNUM_FIELD_COUNT, sb.fieldAt(i)); - assertEquals("Currency field second", UNUM_CURRENCY_FIELD, sb.fieldAt(i + str.length())); + assertEquals("Null field first", (Field) UNUM_FIELD_COUNT, sb.fieldAt(i)); + assertEquals("Currency field second", (Field) UNUM_CURRENCY_FIELD, sb.fieldAt(i + str.length())); } // Very basic FieldPosition test. More robust tests happen in NumberFormatTest. @@ -200,7 +200,7 @@ void NumberStringBuilderTest::testFields() { sb.insertCodePoint(2, 100, UNUM_INTEGER_FIELD, status); assertSuccess("Inserting code point into sb", status); assertEquals("New length", str.length() * 2 + 1, sb.length()); - assertEquals("Integer field", UNUM_INTEGER_FIELD, sb.fieldAt(2)); + assertEquals("Integer field", (Field) UNUM_INTEGER_FIELD, sb.fieldAt(2)); } NumberStringBuilder old(sb); @@ -210,7 +210,7 @@ void NumberStringBuilderTest::testFields() { int32_t numCurr = 0; int32_t numInt = 0; for (int32_t i = 0; i < sb.length(); i++) { - UNumberFormatFields field = sb.fieldAt(i); + Field field = sb.fieldAt(i); assertEquals("Field should equal location in old", old.fieldAt(i % old.length()), field); if (field == UNUM_FIELD_COUNT) { numNull++; diff --git a/icu4c/source/test/intltest/reldatefmttest.cpp b/icu4c/source/test/intltest/reldatefmttest.cpp index 4481f420cb8..f95e4bbc24d 100644 --- a/icu4c/source/test/intltest/reldatefmttest.cpp +++ b/icu4c/source/test/intltest/reldatefmttest.cpp @@ -22,7 +22,9 @@ #include "unicode/localpointer.h" #include "unicode/numfmt.h" #include "unicode/reldatefmt.h" +#include "unicode/rbnf.h" #include "cmemory.h" +#include "itformat.h" static const char *DirectionStr(UDateDirection direction); static const char *RelativeUnitStr(UDateRelativeUnit unit); @@ -741,7 +743,7 @@ static WithQuantityExpectedRelativeDateTimeUnit kEnglishFormat[] = { }; -class RelativeDateTimeFormatterTest : public IntlTest { +class RelativeDateTimeFormatterTest : public IntlTestWithFieldPosition { public: RelativeDateTimeFormatterTest() { } @@ -768,6 +770,9 @@ private: void TestFormat(); void TestFormatNumeric(); void TestLocales(); + void TestFields(); + void TestRBNF(); + void RunTest( const Locale& locale, const WithQuantityExpected* expectedResults, @@ -858,6 +863,8 @@ void RelativeDateTimeFormatterTest::runIndexedTest( TESTCASE_AUTO(TestFormat); TESTCASE_AUTO(TestFormatNumeric); TESTCASE_AUTO(TestLocales); + TESTCASE_AUTO(TestFields); + TESTCASE_AUTO(TestRBNF); TESTCASE_AUTO_END; } @@ -1313,6 +1320,137 @@ void RelativeDateTimeFormatterTest::TestLocales() { } } +void RelativeDateTimeFormatterTest::TestFields() { + IcuTestErrorCode status(*this, "TestFields"); + + RelativeDateTimeFormatter fmt("en-US", status); + + { + const char16_t* message = u"automatic absolute unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(1, UDAT_REL_UNIT_DAY, status); + const char16_t* expectedString = u"tomorrow"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 8}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + { + const char16_t* message = u"automatic numeric unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(3, UDAT_REL_UNIT_DAY, status); + const char16_t* expectedString = u"in 3 days"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 2}, + {UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, 3, 4}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 3, 4}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 5, 9}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + { + const char16_t* message = u"manual absolute unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(UDAT_DIRECTION_NEXT, UDAT_ABSOLUTE_MONDAY, status); + const char16_t* expectedString = u"next Monday"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 11}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + { + const char16_t* message = u"manual numeric unit"; + FormattedRelativeDateTime fv = fmt.formatNumericToValue(1.5, UDAT_REL_UNIT_WEEK, status); + const char16_t* expectedString = u"in 1.5 weeks"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 2}, + {UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, 3, 4}, + {UFIELD_CATEGORY_NUMBER, UNUM_DECIMAL_SEPARATOR_FIELD, 4, 5}, + {UFIELD_CATEGORY_NUMBER, UNUM_FRACTION_FIELD, 5, 6}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 3, 6}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 7, 12}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + { + const char16_t* message = u"manual numeric resolved unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(12, UDAT_DIRECTION_LAST, UDAT_RELATIVE_HOURS, status); + const char16_t* expectedString = u"12 hours ago"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, 0, 2}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 0, 2}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 3, 12}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } + + // Test when the number field is at the end + fmt = RelativeDateTimeFormatter("sw", status); + { + const char16_t* message = u"numeric field at end"; + FormattedRelativeDateTime fv = fmt.formatToValue(12, UDAT_REL_UNIT_HOUR, status); + const char16_t* expectedString = u"baada ya saa 12"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 0, 12}, + {UFIELD_CATEGORY_NUMBER, UNUM_INTEGER_FIELD, 13, 15}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 13, 15}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } +} + +void RelativeDateTimeFormatterTest::TestRBNF() { + IcuTestErrorCode status(*this, "TestRBNF"); + + LocalPointer rbnf(new RuleBasedNumberFormat(URBNF_SPELLOUT, "en-us", status)); + RelativeDateTimeFormatter fmt("en-us", rbnf.orphan(), status); + UnicodeString result; + assertEquals("format (direction)", "in five seconds", + fmt.format(5, UDAT_DIRECTION_NEXT, UDAT_RELATIVE_SECONDS, result, status)); + assertEquals("formatNumeric", "one week ago", + fmt.formatNumeric(-1, UDAT_REL_UNIT_WEEK, result.remove(), status)); + assertEquals("format (absolute)", "yesterday", + fmt.format(UDAT_DIRECTION_LAST, UDAT_ABSOLUTE_DAY, result.remove(), status)); + assertEquals("format (relative)", "in forty-two months", + fmt.format(42, UDAT_REL_UNIT_MONTH, result.remove(), status)); + + { + const char16_t* message = u"formatToValue (relative)"; + FormattedRelativeDateTime fv = fmt.formatToValue(-100, UDAT_REL_UNIT_YEAR, status); + const char16_t* expectedString = u"one hundred years ago"; + static const UFieldPositionWithCategory expectedFieldPositions[] = { + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_NUMERIC_FIELD, 0, 11}, + {UFIELD_CATEGORY_RELATIVE_DATETIME, UDAT_REL_LITERAL_FIELD, 12, 21}}; + checkMixedFormattedValue( + message, + fv, + expectedString, + expectedFieldPositions, + UPRV_LENGTHOF(expectedFieldPositions)); + } +} + static const char *kLast2 = "Last_2"; static const char *kLast = "Last"; static const char *kThis = "This"; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java index fc881a2c504..c82d50d7e01 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantAffixModifier.java @@ -2,7 +2,7 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; -import com.ibm.icu.text.NumberFormat.Field; +import java.text.Format.Field; /** * The canonical implementation of {@link Modifier}, containing a prefix and suffix string. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java index d53349204be..673296005a8 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java @@ -2,10 +2,9 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import java.text.Format.Field; import java.util.Arrays; -import com.ibm.icu.text.NumberFormat.Field; - /** * An implementation of {@link Modifier} that allows for multiple types of fields in the same modifier. * Constructed based on the contents of two {@link NumberStringBuilder} instances (one for the prefix, diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java index 2cf875884e5..61c04c53a4a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java @@ -2,6 +2,8 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import java.text.Format.Field; + import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.UnicodeSet; @@ -122,7 +124,7 @@ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix. // This works even if the last code point in the prefix is 2 code units because the // field value gets populated to both indices in the field array. - NumberFormat.Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) + Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index); if (affixField != NumberFormat.Field.CURRENCY) { return 0; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java index d0e74bbe093..f3f4635fedb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java @@ -2,8 +2,9 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import java.text.Format.Field; + import com.ibm.icu.impl.StandardPlural; -import com.ibm.icu.text.NumberFormat.Field; /** * A Modifier is an object that can be passed through the formatting pipeline until it is finally applied diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java index d8875610bf8..4cdde30d78c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/MutablePatternModifier.java @@ -326,7 +326,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr } @Override - public boolean containsField(Field field) { + public boolean containsField(java.text.Format.Field field) { // This method is not currently used. (unsafe path not used in range formatting) assert false; return false; 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 5102db4b518..0380b0e59b7 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 @@ -5,15 +5,14 @@ package com.ibm.icu.impl.number; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.text.FieldPosition; +import java.text.Format.Field; import java.util.Arrays; 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; /** @@ -521,7 +520,7 @@ public class NumberStringBuilder implements CharSequence { ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); cfpos.constrainField(rawField); cfpos.setState(rawField, null, fp.getBeginIndex(), fp.getEndIndex()); - if (nextPosition(cfpos)) { + if (nextPosition(cfpos, null)) { fp.setBeginIndex(cfpos.getStart()); fp.setEndIndex(cfpos.getLimit()); return true; @@ -545,28 +544,38 @@ public class NumberStringBuilder implements CharSequence { return false; } - public AttributedCharacterIterator toCharacterIterator() { + public AttributedCharacterIterator toCharacterIterator(Field numericField) { ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition(); AttributedString as = new AttributedString(toString()); - while (this.nextPosition(cfpos)) { + while (this.nextPosition(cfpos, numericField)) { // Backwards compatibility: field value = field as.addAttribute(cfpos.getField(), cfpos.getField(), cfpos.getStart(), cfpos.getLimit()); } return as.getIterator(); } - public boolean nextPosition(ConstrainedFieldPosition cfpos) { - if (cfpos.getConstraintType() == ConstraintType.CLASS - && !cfpos.getClassConstraint().isAssignableFrom(NumberFormat.Field.class)) { - return false; + static class NullField extends Field { + private static final long serialVersionUID = 1L; + static final NullField END = new NullField("end"); + private NullField(String name) { + super(name); } + } - boolean isSearchingForField = (cfpos.getConstraintType() == ConstraintType.FIELD); - + /** + * Implementation of nextPosition consistent with the contract of FormattedValue. + * + * @param cfpos + * The argument passed to the public API. + * @param numericField + * Optional. If non-null, apply this field to the entire numeric portion of the string. + * @return See FormattedValue#nextPosition. + */ + public boolean nextPosition(ConstrainedFieldPosition cfpos, Field numericField) { int fieldStart = -1; Field currField = null; for (int i = zero + cfpos.getLimit(); i <= zero + length; i++) { - Field _field = (i < zero + length) ? fields[i] : null; + Field _field = (i < zero + length) ? fields[i] : NullField.END; // Case 1: currently scanning a field. if (currField != null) { if (currField != _field) { @@ -592,9 +601,10 @@ public class NumberStringBuilder implements CharSequence { continue; } // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER. - if ((!isSearchingForField || cfpos.getField() == NumberFormat.Field.INTEGER) + if (cfpos.matchesField(NumberFormat.Field.INTEGER) && i > zero - && i - zero > cfpos.getLimit() // don't return the same field twice in a row + // don't return the same field twice in a row: + && i - zero > cfpos.getLimit() && isIntOrGroup(fields[i - 1]) && !isIntOrGroup(_field)) { int j = i - 1; @@ -602,16 +612,29 @@ public class NumberStringBuilder implements CharSequence { cfpos.setState(NumberFormat.Field.INTEGER, null, j - zero + 1, i - zero); return true; } + // Special case: coalesce NUMERIC if we are pointing at the end of the NUMERIC. + if (numericField != null + && cfpos.matchesField(numericField) + && i > zero + // don't return the same field twice in a row: + && (i - zero > cfpos.getLimit() || cfpos.getField() != numericField) + && isNumericField(fields[i - 1]) + && !isNumericField(_field)) { + int j = i - 1; + for (; j >= zero && isNumericField(fields[j]); j--) {} + cfpos.setState(numericField, 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) { + if (_field == null || _field == NullField.END) { continue; } // Case 3: check for field starting at this position - if (!isSearchingForField || cfpos.getField() == _field) { + if (cfpos.matchesField(_field)) { fieldStart = i - zero; currField = _field; } @@ -621,10 +644,14 @@ public class NumberStringBuilder implements CharSequence { return false; } - private static boolean isIntOrGroup(NumberFormat.Field field) { + private static boolean isIntOrGroup(Field field) { return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR; } + private static boolean isNumericField(Field field) { + return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass()); + } + private int trimBack(int limit) { return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES) .spanBack(this, limit, UnicodeSet.SpanCondition.CONTAINED); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java index 30c12d61a82..26cb275c5f7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java @@ -2,9 +2,10 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number; +import java.text.Format.Field; + import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper; -import com.ibm.icu.text.NumberFormat.Field; import com.ibm.icu.util.ICUException; /** @@ -65,7 +66,7 @@ public class SimpleModifier implements Modifier { @Override public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { - return formatAsPrefixSuffix(output, leftIndex, rightIndex, field); + return formatAsPrefixSuffix(output, leftIndex, rightIndex); } @Override @@ -139,8 +140,7 @@ public class SimpleModifier implements Modifier { public int formatAsPrefixSuffix( NumberStringBuilder result, int startIndex, - int endIndex, - Field field) { + int endIndex) { if (suffixOffset == -1) { // There is no argument for the inner number; overwrite the entire segment with our string. return result.splice(startIndex, endIndex, compiledPattern, 2, 2 + prefixLength, field); 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 24a78ae04a9..09da4247e37 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 @@ -101,7 +101,7 @@ public class FormattedNumber implements FormattedValue { */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { - return nsb.nextPosition(cfpos); + return nsb.nextPosition(cfpos, null); } /** @@ -150,7 +150,7 @@ public class FormattedNumber implements FormattedValue { */ @Override public AttributedCharacterIterator toCharacterIterator() { - return nsb.toCharacterIterator(); + return nsb.toCharacterIterator(null); } /** 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 56f2481fae6..3b37559fe6a 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 @@ -107,7 +107,7 @@ public class FormattedNumberRange implements FormattedValue { */ @Override public boolean nextPosition(ConstrainedFieldPosition cfpos) { - return string.nextPosition(cfpos); + return string.nextPosition(cfpos, null); } /** @@ -151,7 +151,7 @@ public class FormattedNumberRange implements FormattedValue { */ @Override public AttributedCharacterIterator toCharacterIterator() { - return string.toCharacterIterator(); + return string.toCharacterIterator(null); } /** diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java index 6452130d277..281aca5983e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/LocalizedNumberFormatter.java @@ -133,6 +133,13 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings * QuanitityFormatter appears here instead of in com.ibm.icu.impl because it depends on * PluralRules and DecimalFormat. It is package-protected as it is not meant for public use. @@ -101,24 +100,6 @@ class QuantityFormatter { return StandardPlural.orOtherFromString(pluralKeyword); } - /** - * Selects the standard plural form for the number/formatter/rules. - */ - public static StandardPlural selectPlural( - Number number, NumberFormat fmt, PluralRules rules, - StringBuffer formattedNumber, FieldPosition pos) { - UFieldPosition fpos = new UFieldPosition(pos.getFieldAttribute(), pos.getField()); - fmt.format(number, formattedNumber, fpos); - // TODO: Long, BigDecimal & BigInteger may not fit into doubleValue(). - FixedDecimal fd = new FixedDecimal( - number.doubleValue(), - fpos.getCountVisibleFractionDigits(), fpos.getFractionDigits()); - String pluralKeyword = rules.select(fd); - pos.setBeginIndex(fpos.getBeginIndex()); - pos.setEndIndex(fpos.getEndIndex()); - return StandardPlural.orOtherFromString(pluralKeyword); - } - /** * Formats the pattern with the value and adjusts the FieldPosition. */ diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java index 3cba6ac371a..f451fdfa455 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java @@ -8,20 +8,28 @@ */ package com.ibm.icu.text; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.text.AttributedCharacterIterator; +import java.text.Format; import java.util.EnumMap; import java.util.Locale; import com.ibm.icu.impl.CacheBase; -import com.ibm.icu.impl.DontCareFieldPosition; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.SimpleFormatterImpl; import com.ibm.icu.impl.SoftCache; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.UResource; +import com.ibm.icu.impl.number.DecimalQuantity; +import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.SimpleModifier; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.ICUException; +import com.ibm.icu.util.ICUUncheckedIOException; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; @@ -386,6 +394,156 @@ public final class RelativeDateTimeFormatter { SATURDAY, } + /** + * Field constants used when accessing field information for relative + * datetime strings in FormattedValue. + *

+ * There is no public constructor to this class; the only instances are the + * constants defined here. + *

+ * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static class Field extends Format.Field { + private static final long serialVersionUID = -5327685528663492325L; + + /** + * Represents a literal text string, like "tomorrow" or "days ago". + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static final Field LITERAL = new Field("literal"); + + /** + * Represents a number quantity, like "3" in "3 days ago". + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static final Field NUMERIC = new Field("numeric"); + + private Field(String fieldName) { + super(fieldName); + } + + /** + * Serizalization method resolve instances to the constant Field values + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + protected Object readResolve() throws InvalidObjectException { + if (this.getName().equals(LITERAL.getName())) + return LITERAL; + if (this.getName().equals(NUMERIC.getName())) + return NUMERIC; + + throw new InvalidObjectException("An invalid object."); + } + } + + /** + * Represents the result of a formatting operation of a relative datetime. + * Access the string value or field information. + * + * @author sffc + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public static class FormattedRelativeDateTime implements FormattedValue { + + private final NumberStringBuilder string; + + private FormattedRelativeDateTime(NumberStringBuilder string) { + this.string = string; + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public String toString() { + return string.toString(); + } + + /** + * {@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 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 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 A appendTo(A appendable) { + try { + appendable.append(string); + } catch (IOException e) { + // Throw as an unchecked exception to avoid users needing try/catch + throw new ICUUncheckedIOException(e); + } + return appendable; + } + + /** + * {@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, Field.NUMERIC); + } + + /** + * {@inheritDoc} + * + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + @Override + public AttributedCharacterIterator toCharacterIterator() { + return string.toCharacterIterator(Field.NUMERIC); + } + } + /** * Returns a RelativeDateTimeFormatter for the default locale. * @stable ICU 53 @@ -482,7 +640,11 @@ public final class RelativeDateTimeFormatter { /** * Formats a relative date with a quantity such as "in 5 days" or - * "3 months ago" + * "3 months ago". + * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param quantity The numerical amount e.g 5. This value is formatted * according to this object's {@link NumberFormat} object. * @param direction NEXT means a future relative date; LAST means a past @@ -494,25 +656,57 @@ public final class RelativeDateTimeFormatter { * @stable ICU 53 */ public String format(double quantity, Direction direction, RelativeUnit unit) { + NumberStringBuilder output = formatImpl(quantity, direction, unit); + return adjustForContext(output.toString()); + } + + /** + * Formats a relative date with a quantity such as "in 5 days" or + * "3 months ago". + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * @param quantity The numerical amount e.g 5. This value is formatted + * according to this object's {@link NumberFormat} object. + * @param direction NEXT means a future relative date; LAST means a past + * relative date. + * @param unit the unit e.g day? month? year? + * @return the formatted relative datetime + * @throws IllegalArgumentException if direction is something other than + * NEXT or LAST. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public FormattedRelativeDateTime formatToValue(double quantity, Direction direction, RelativeUnit unit) { + checkNoAdjustForContext(); + return new FormattedRelativeDateTime(formatImpl(quantity, direction, unit)); + } + + /** Implementation method for format and formatToValue with RelativeUnit */ + private NumberStringBuilder formatImpl(double quantity, Direction direction, RelativeUnit unit) { if (direction != Direction.LAST && direction != Direction.NEXT) { throw new IllegalArgumentException("direction must be NEXT or LAST"); } - String result; int pastFutureIndex = (direction == Direction.NEXT ? 1 : 0); - // This class is thread-safe, yet numberFormat is not. To ensure thread-safety of this - // class we must guarantee that only one thread at a time uses our numberFormat. - synchronized (numberFormat) { - StringBuffer formatStr = new StringBuffer(); - DontCareFieldPosition fieldPosition = DontCareFieldPosition.INSTANCE; - StandardPlural pluralForm = QuantityFormatter.selectPlural(quantity, - numberFormat, pluralRules, formatStr, fieldPosition); - - String formatter = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); - result = SimpleFormatterImpl.formatCompiledPattern(formatter, formatStr); + NumberStringBuilder output = new NumberStringBuilder(); + String pluralKeyword; + if (numberFormat instanceof DecimalFormat) { + DecimalQuantity dq = new DecimalQuantity_DualStorageBCD(quantity); + ((DecimalFormat) numberFormat).toNumberFormatter().formatImpl(dq, output); + pluralKeyword = pluralRules.select(dq); + } else { + String result = numberFormat.format(quantity); + output.append(result, null); + pluralKeyword = pluralRules.select(quantity); } - return adjustForContext(result); + StandardPlural pluralForm = StandardPlural.orOtherFromString(pluralKeyword); + String compiledPattern = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm); + SimpleModifier modifier = new SimpleModifier(compiledPattern, Field.LITERAL, false); + modifier.formatAsPrefixSuffix(output, 0, output.length()); + return output; } /** @@ -520,6 +714,9 @@ public final class RelativeDateTimeFormatter { * using a numeric style, e.g. "1 week ago", "in 1 week", * "5 weeks ago", "in 5 weeks". * + * This method returns a String. To get more information about the + * formatting result, use formatNumericToValue(). + * * @param offset The signed offset for the specified unit. This * will be formatted according to this object's * NumberFormat object. @@ -530,6 +727,35 @@ public final class RelativeDateTimeFormatter { * @stable ICU 57 */ public String formatNumeric(double offset, RelativeDateTimeUnit unit) { + NumberStringBuilder output = formatNumericImpl(offset, unit); + return adjustForContext(output.toString()); + } + + /** + * Format a combination of RelativeDateTimeUnit and numeric offset + * using a numeric style, e.g. "1 week ago", "in 1 week", + * "5 weeks ago", "in 5 weeks". + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by formatNumeric(). + * + * @param offset The signed offset for the specified unit. This + * will be formatted according to this object's + * NumberFormat object. + * @param unit The unit to use when formatting the relative + * date, e.g. RelativeDateTimeUnit.WEEK, + * RelativeDateTimeUnit.FRIDAY. + * @return The formatted string (may be empty in case of error) + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public FormattedRelativeDateTime formatNumericToValue(double offset, RelativeDateTimeUnit unit) { + checkNoAdjustForContext(); + return new FormattedRelativeDateTime(formatNumericImpl(offset, unit)); + } + + /** Implementation method for formatNumeric and formatNumericToValue */ + private NumberStringBuilder formatNumericImpl(double offset, RelativeDateTimeUnit unit) { // TODO: // The full implementation of this depends on CLDR data that is not yet available, // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data. @@ -555,8 +781,7 @@ public final class RelativeDateTimeFormatter { direction = Direction.LAST; offset = -offset; } - String result = format(offset, direction, relunit); - return (result != null)? result: ""; + return formatImpl(offset, direction, relunit); } private int[] styleToDateFormatSymbolsWidth = { @@ -565,6 +790,10 @@ public final class RelativeDateTimeFormatter { /** * Formats a relative date without a quantity. + * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param direction NEXT, LAST, THIS, etc. * @param unit e.g SATURDAY, DAY, MONTH * @return the formatted string. If direction has a value that is documented as not being @@ -575,6 +804,39 @@ public final class RelativeDateTimeFormatter { * @stable ICU 53 */ public String format(Direction direction, AbsoluteUnit unit) { + String result = formatAbsoluteImpl(direction, unit); + return result != null ? adjustForContext(result) : null; + } + + /** + * Formats a relative date without a quantity. + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * @param direction NEXT, LAST, THIS, etc. + * @param unit e.g SATURDAY, DAY, MONTH + * @return the formatted string. If direction has a value that is documented as not being + * fully supported in every locale (for example NEXT_2 or LAST_2) then this function may + * return null to signal that no formatted string is available. + * @throws IllegalArgumentException if the direction is incompatible with + * unit this can occur with NOW which can only take PLAIN. + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public FormattedRelativeDateTime formatToValue(Direction direction, AbsoluteUnit unit) { + checkNoAdjustForContext(); + String string = formatAbsoluteImpl(direction, unit); + if (string == null) { + return null; + } + NumberStringBuilder nsb = new NumberStringBuilder(); + nsb.append(string, Field.LITERAL); + return new FormattedRelativeDateTime(nsb); + } + + /** Implementation method for format and formatToValue with AbsoluteUnit */ + private String formatAbsoluteImpl(Direction direction, AbsoluteUnit unit) { if (unit == AbsoluteUnit.NOW && direction != Direction.PLAIN) { throw new IllegalArgumentException("NOW can only accept direction PLAIN."); } @@ -592,7 +854,7 @@ public final class RelativeDateTimeFormatter { // Not PLAIN, or not a weekday. result = getAbsoluteUnitString(style, unit, direction); } - return result != null ? adjustForContext(result) : null; + return result; } /** @@ -602,6 +864,9 @@ public final class RelativeDateTimeFormatter { * style if no appropriate text term is available for the specified * offset in the object’s locale. * + * This method returns a String. To get more information about the + * formatting result, use formatToValue(). + * * @param offset The signed offset for the specified field. * @param unit The unit to use when formatting the relative * date, e.g. RelativeDateTimeUnit.WEEK, @@ -610,6 +875,43 @@ public final class RelativeDateTimeFormatter { * @stable ICU 57 */ public String format(double offset, RelativeDateTimeUnit unit) { + return adjustForContext(formatRelativeImpl(offset, unit).toString()); + } + + /** + * Format a combination of RelativeDateTimeUnit and numeric offset + * using a text style if possible, e.g. "last week", "this week", + * "next week", "yesterday", "tomorrow". Falls back to numeric + * style if no appropriate text term is available for the specified + * offset in the object’s locale. + * + * This method returns a FormattedRelativeDateTime, which exposes more + * information than the String returned by format(). + * + * @param offset The signed offset for the specified field. + * @param unit The unit to use when formatting the relative + * date, e.g. RelativeDateTimeUnit.WEEK, + * RelativeDateTimeUnit.FRIDAY. + * @return The formatted string (may be empty in case of error) + * @draft ICU 64 + * @provisional This API might change or be removed in a future release. + */ + public FormattedRelativeDateTime formatToValue(double offset, RelativeDateTimeUnit unit) { + checkNoAdjustForContext(); + CharSequence cs = formatRelativeImpl(offset, unit); + NumberStringBuilder nsb; + if (cs instanceof NumberStringBuilder) { + nsb = (NumberStringBuilder) cs; + } else { + nsb = new NumberStringBuilder(); + nsb.append(cs, Field.LITERAL); + } + return new FormattedRelativeDateTime(nsb); + } + + + /** Implementation method for format and formatToValue with RelativeDateTimeUnit. */ + private CharSequence formatRelativeImpl(double offset, RelativeDateTimeUnit unit) { // TODO: // The full implementation of this depends on CLDR data that is not yet available, // see: http://unicode.org/cldr/trac/ticket/9165 Add more relative field data. @@ -661,13 +963,13 @@ public final class RelativeDateTimeFormatter { break; } if (!useNumeric) { - String result = format(direction, absunit); + String result = formatAbsoluteImpl(direction, absunit); if (result != null && result.length() > 0) { return result; } } // otherwise fallback to formatNumeric - return formatNumeric(offset, unit); + return formatNumericImpl(offset, unit); } /** @@ -756,6 +1058,12 @@ public final class RelativeDateTimeFormatter { } } + private void checkNoAdjustForContext() { + if (breakIterator != null) { + throw new UnsupportedOperationException("Capitalization context is not supported in formatV"); + } + } + private RelativeDateTimeFormatter( EnumMap>> qualitativeUnitMap, EnumMap> patternMap, 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 index fc50106503d..0811a7a1334 100644 --- 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 @@ -128,7 +128,7 @@ public class FormattedValueTest { String baseMessage = message + ": " + fv.toString() + ": "; // Check the String and CharSequence - assertEquals(baseMessage + "string", expectedString, fv.toString()); + assertEquals(baseMessage + " string", expectedString, fv.toString()); assertCharSequenceEquals(expectedString, fv); // Check the AttributedCharacterIterator @@ -159,7 +159,8 @@ public class FormattedValueTest { 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 field: " + i + ": " + currentAttributes, + 0, attributesRemaining); } assertEquals(baseMessage + "Should have looked at every character", stringLength, i); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java index a7c14d418ea..bada1be0049 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/RelativeDateTimeFormatterTest.java @@ -21,9 +21,11 @@ import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.RelativeDateTimeFormatter; import com.ibm.icu.text.RelativeDateTimeFormatter.AbsoluteUnit; import com.ibm.icu.text.RelativeDateTimeFormatter.Direction; +import com.ibm.icu.text.RelativeDateTimeFormatter.FormattedRelativeDateTime; import com.ibm.icu.text.RelativeDateTimeFormatter.RelativeDateTimeUnit; import com.ibm.icu.text.RelativeDateTimeFormatter.RelativeUnit; import com.ibm.icu.text.RelativeDateTimeFormatter.Style; +import com.ibm.icu.text.RuleBasedNumberFormat; import com.ibm.icu.util.ULocale; @RunWith(JUnit4.class) @@ -1025,12 +1027,101 @@ public class RelativeDateTimeFormatterTest extends TestFmwk { assertEquals("narrow: in 6 qtr", "in 6 qtr", w); } -@Test -public void TestLocales() { - ULocale[] availableLocales = ULocale.getAvailableLocales(); - for (ULocale loc: availableLocales) { - RelativeDateTimeFormatter.getInstance(loc); + @Test + public void TestLocales() { + ULocale[] availableLocales = ULocale.getAvailableLocales(); + for (ULocale loc: availableLocales) { + RelativeDateTimeFormatter.getInstance(loc); + } + } + + @Test + public void TestFields() { + RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(ULocale.US); + + { + String message = "automatic absolute unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(1, RelativeDateTimeUnit.DAY); + String expectedString = "tomorrow"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.LITERAL, 0, 8}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + { + String message = "automatic numeric unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(3, RelativeDateTimeUnit.DAY); + String expectedString = "in 3 days"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.LITERAL, 0, 2}, + {NumberFormat.Field.INTEGER, 3, 4}, + {RelativeDateTimeFormatter.Field.NUMERIC, 3, 4}, + {RelativeDateTimeFormatter.Field.LITERAL, 5, 9}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + { + String message = "manual absolute unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(Direction.NEXT, AbsoluteUnit.MONDAY); + String expectedString = "next Monday"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.LITERAL, 0, 11}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + { + String message = "manual numeric unit"; + FormattedRelativeDateTime fv = fmt.formatNumericToValue(1.5, RelativeDateTimeUnit.WEEK); + String expectedString = "in 1.5 weeks"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.LITERAL, 0, 2}, + {NumberFormat.Field.INTEGER, 3, 4}, + {NumberFormat.Field.DECIMAL_SEPARATOR, 4, 5}, + {NumberFormat.Field.FRACTION, 5, 6}, + {RelativeDateTimeFormatter.Field.NUMERIC, 3, 6}, + {RelativeDateTimeFormatter.Field.LITERAL, 7, 12}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + { + String message = "manual numeric resolved unit"; + FormattedRelativeDateTime fv = fmt.formatToValue(12, Direction.LAST, RelativeUnit.HOURS); + String expectedString = "12 hours ago"; + Object[][] expectedFieldPositions = new Object[][]{ + {NumberFormat.Field.INTEGER, 0, 2}, + {RelativeDateTimeFormatter.Field.NUMERIC, 0, 2}, + {RelativeDateTimeFormatter.Field.LITERAL, 3, 12}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + + // Test when the number field is at the end + fmt = RelativeDateTimeFormatter.getInstance(new ULocale("sw")); + { + String message = "numeric field at end"; + FormattedRelativeDateTime fv = fmt.formatToValue(12, RelativeDateTimeUnit.HOUR); + String expectedString = "baada ya saa 12"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.LITERAL, 0, 12}, + {NumberFormat.Field.INTEGER, 13, 15}, + {RelativeDateTimeFormatter.Field.NUMERIC, 13, 15}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } + } + + @Test + public void TestRBNF() { + RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ULocale.US, RuleBasedNumberFormat.SPELLOUT); + RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance(ULocale.US, rbnf); + assertEquals("format (direction)", "in five seconds", fmt.format(5, Direction.NEXT, RelativeUnit.SECONDS)); + assertEquals("formatNumeric", "one week ago", fmt.formatNumeric(-1, RelativeDateTimeUnit.WEEK)); + assertEquals("format (absolute)", "yesterday", fmt.format(Direction.LAST, AbsoluteUnit.DAY)); + assertEquals("format (relative)", "in forty-two months", fmt.format(42, RelativeDateTimeUnit.MONTH)); + + { + String message = "formatToValue (relative)"; + FormattedRelativeDateTime fv = fmt.formatToValue(-100, RelativeDateTimeUnit.YEAR); + String expectedString = "one hundred years ago"; + Object[][] expectedFieldPositions = new Object[][]{ + {RelativeDateTimeFormatter.Field.NUMERIC, 0, 11}, + {RelativeDateTimeFormatter.Field.LITERAL, 12, 21}}; + FormattedValueTest.checkFormattedValue(message, fv, expectedString, expectedFieldPositions); + } } -} } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java index 3b7199ee9a6..9271d94b49a 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java @@ -22,6 +22,7 @@ import com.ibm.icu.dev.test.TestFmwk; import com.ibm.icu.impl.number.DecimalFormatProperties; import com.ibm.icu.impl.number.DecimalQuantity; import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; +import com.ibm.icu.impl.number.NumberStringBuilder; import com.ibm.icu.impl.number.RoundingUtils; import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.NumberFormatter; @@ -235,8 +236,12 @@ public class DecimalQuantityTest extends TestFmwk { for (LocalizedNumberFormatter format : formats) { DecimalQuantity q0 = rq0.createCopy(); DecimalQuantity q1 = rq1.createCopy(); - String s1 = format.format(q0).toString(); - String s2 = format.format(q1).toString(); + NumberStringBuilder nsb1 = new NumberStringBuilder(); + NumberStringBuilder nsb2 = new NumberStringBuilder(); + format.formatImpl(q0, nsb1); + format.formatImpl(q1, nsb2); + String s1 = nsb1.toString(); + String s2 = nsb2.toString(); assertEquals("Different output from formatter (" + q0 + ", " + q1 + ")", s1, s2); } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java index 7bb5e98fc8a..d34b64453fe 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java @@ -34,6 +34,7 @@ import com.ibm.icu.text.MessageFormat; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralFormat; import com.ibm.icu.text.PluralRules; +import com.ibm.icu.text.RelativeDateTimeFormatter; import com.ibm.icu.text.RuleBasedNumberFormat; import com.ibm.icu.text.SelectFormat; import com.ibm.icu.text.SimpleDateFormat; @@ -1800,6 +1801,21 @@ public class FormatHandler } } + public static class RelativeDateTimeFormatterFieldHandler implements SerializableTestUtility.Handler + { + @Override + public Object[] getTestObjects() + { + return new Object[] {RelativeDateTimeFormatter.Field.LITERAL}; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) + { + return (a == b); + } + } + public static class DateFormatHandler implements SerializableTestUtility.Handler { static HashMap cannedPatterns = new HashMap(); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java index c1e97c771b3..fe9a601709c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java @@ -818,6 +818,7 @@ public class SerializableTestUtility { map.put("com.ibm.icu.text.DateFormat$Field", new FormatHandler.DateFormatFieldHandler()); map.put("com.ibm.icu.text.ChineseDateFormat$Field", new FormatHandler.ChineseDateFormatFieldHandler()); map.put("com.ibm.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler()); + map.put("com.ibm.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler()); map.put("com.ibm.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler()); map.put("com.ibm.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler());