From: Travis Keep Date: Wed, 10 Sep 2014 20:06:52 +0000 (+0000) Subject: ICU-10999 Add per unit measure formatting. X-Git-Tag: milestone-59-0-1~1565 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c794caf7d45b7a92ef5e91befe4ff60d623334b1;p=icu ICU-10999 Add per unit measure formatting. X-SVN-Rev: 36441 --- diff --git a/icu4c/source/common/simplepatternformatter.h b/icu4c/source/common/simplepatternformatter.h index 4a43c37d9d6..b286a79cc35 100644 --- a/icu4c/source/common/simplepatternformatter.h +++ b/icu4c/source/common/simplepatternformatter.h @@ -96,6 +96,13 @@ public: */ UBool startsWithPlaceholder(int32_t id) const; + /** + * Returns this pattern with none of the placeholders. + */ + const UnicodeString &getPatternWithNoPlaceholders() const { + return noPlaceholders; + } + /** * Formats given value. */ diff --git a/icu4c/source/i18n/measfmt.cpp b/icu4c/source/i18n/measfmt.cpp index d2c1ba3e55a..e13b04fc5a7 100644 --- a/icu4c/source/i18n/measfmt.cpp +++ b/icu4c/source/i18n/measfmt.cpp @@ -17,6 +17,7 @@ #include "unicode/numfmt.h" #include "currfmt.h" #include "unicode/localpointer.h" +#include "simplepatternformatter.h" #include "quantityformatter.h" #include "unicode/plurrule.h" #include "unicode/decimfmt.h" @@ -79,6 +80,8 @@ private: class MeasureFormatCacheData : public SharedObject { public: QuantityFormatter formatters[MEAS_UNIT_COUNT][WIDTH_INDEX_COUNT]; + SimplePatternFormatter perFormatters[WIDTH_INDEX_COUNT]; + MeasureFormatCacheData(); void adoptCurrencyFormat(int32_t widthIndex, NumberFormat *nfToAdopt) { delete currencyFormats[widthIndex]; @@ -101,11 +104,23 @@ public: const NumericDateFormatters *getNumericDateFormatters() const { return numericDateFormatters; } + void adoptPerUnitFormatter( + int32_t index, + int32_t widthIndex, + SimplePatternFormatter *formatterToAdopt) { + delete perUnitFormatters[index][widthIndex]; + perUnitFormatters[index][widthIndex] = formatterToAdopt; + } + const SimplePatternFormatter * const * getPerUnitFormattersByIndex( + int32_t index) const { + return perUnitFormatters[index]; + } virtual ~MeasureFormatCacheData(); private: NumberFormat *currencyFormats[WIDTH_INDEX_COUNT]; NumberFormat *integerFormat; NumericDateFormatters *numericDateFormatters; + SimplePatternFormatter *perUnitFormatters[MEAS_UNIT_COUNT][WIDTH_INDEX_COUNT]; MeasureFormatCacheData(const MeasureFormatCacheData &other); MeasureFormatCacheData &operator=(const MeasureFormatCacheData &other); }; @@ -114,6 +129,11 @@ MeasureFormatCacheData::MeasureFormatCacheData() { for (int32_t i = 0; i < UPRV_LENGTHOF(currencyFormats); ++i) { currencyFormats[i] = NULL; } + for (int32_t i = 0; i < MEAS_UNIT_COUNT; ++i) { + for (int32_t j = 0; j < WIDTH_INDEX_COUNT; ++j) { + perUnitFormatters[i][j] = NULL; + } + } integerFormat = NULL; numericDateFormatters = NULL; } @@ -122,6 +142,11 @@ MeasureFormatCacheData::~MeasureFormatCacheData() { for (int32_t i = 0; i < UPRV_LENGTHOF(currencyFormats); ++i) { delete currencyFormats[i]; } + for (int32_t i = 0; i < MEAS_UNIT_COUNT; ++i) { + for (int32_t j = 0; j < WIDTH_INDEX_COUNT; ++j) { + delete perUnitFormatters[i][j]; + } + } delete integerFormat; delete numericDateFormatters; } @@ -185,6 +210,22 @@ static UBool loadMeasureUnitData( status = U_ZERO_ERROR; continue; } + { + // compound per + LocalUResourceBundlePointer compoundPerBundle( + ures_getByKeyWithFallback( + widthBundle.getAlias(), + "compound/per", + NULL, + &status)); + if (U_FAILURE(status)) { + status = U_ZERO_ERROR; + } else { + UnicodeString perPattern; + getString(compoundPerBundle.getAlias(), perPattern, status); + cacheData.perFormatters[currentWidth].compile(perPattern, status); + } + } for (int32_t currentUnit = 0; currentUnit < unitCount; ++currentUnit) { // Be sure status is clear next lookup may fail. if (U_FAILURE(status)) { @@ -224,9 +265,18 @@ static UBool loadMeasureUnitData( return FALSE; } const char * resKey = ures_getKey(pluralBundle.getAlias()); - if (uprv_strcmp(resKey, "dnam") == 0 || uprv_strcmp(resKey, "per") == 0) { + if (uprv_strcmp(resKey, "dnam") == 0) { continue; // skip display name & per pattern (new in CLDR 26 / ICU 54) for now, not part of plurals } + if (uprv_strcmp(resKey, "per") == 0) { + UnicodeString perPattern; + getString(pluralBundle.getAlias(), perPattern, status); + cacheData.adoptPerUnitFormatter( + units[currentUnit].getIndex(), + currentWidth, + new SimplePatternFormatter(perPattern)); + continue; + } UnicodeString rawPattern; getString(pluralBundle.getAlias(), rawPattern, status); cacheData.formatters[units[currentUnit].getIndex()][currentWidth].add( @@ -542,6 +592,31 @@ void MeasureFormat::parseObject( return; } +UnicodeString &MeasureFormat::formatMeasuresPer( + const Measure *measures, + int32_t measureCount, + const MeasureUnit &perUnit, + UnicodeString &appendTo, + FieldPosition &pos, + UErrorCode &status) const { + FieldPosition fpos(pos.getField()); + UnicodeString measuresString; + int32_t offset = withPerUnit( + formatMeasures( + measures, measureCount, measuresString, fpos, status), + perUnit, + appendTo, + status); + if (U_FAILURE(status)) { + return appendTo; + } + if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { + pos.setBeginIndex(fpos.getBeginIndex() + offset); + pos.setEndIndex(fpos.getEndIndex() + offset); + } + return appendTo; +} + UnicodeString &MeasureFormat::formatMeasures( const Measure *measures, int32_t measureCount, @@ -861,6 +936,93 @@ const QuantityFormatter *MeasureFormat::getQuantityFormatter( return NULL; } +const SimplePatternFormatter *MeasureFormat::getPerUnitFormatter( + int32_t index, + int32_t widthIndex) const { + const SimplePatternFormatter * const * perUnitFormatters = + cache->getPerUnitFormattersByIndex(index); + if (perUnitFormatters[widthIndex] != NULL) { + return perUnitFormatters[widthIndex]; + } + if (perUnitFormatters[UMEASFMT_WIDTH_SHORT] != NULL) { + return perUnitFormatters[UMEASFMT_WIDTH_SHORT]; + } + if (perUnitFormatters[UMEASFMT_WIDTH_WIDE] != NULL) { + return perUnitFormatters[UMEASFMT_WIDTH_WIDE]; + } + return NULL; +} + +const SimplePatternFormatter *MeasureFormat::getPerFormatter( + int32_t widthIndex, + UErrorCode &status) const { + if (U_FAILURE(status)) { + return NULL; + } + const SimplePatternFormatter * perFormatters = cache->perFormatters; + + if (perFormatters[widthIndex].getPlaceholderCount() == 2) { + return &perFormatters[widthIndex]; + } + if (perFormatters[UMEASFMT_WIDTH_SHORT].getPlaceholderCount() == 2) { + return &perFormatters[UMEASFMT_WIDTH_SHORT]; + } + if (perFormatters[UMEASFMT_WIDTH_WIDE].getPlaceholderCount() == 2) { + return &perFormatters[UMEASFMT_WIDTH_WIDE]; + } + status = U_MISSING_RESOURCE_ERROR; + return NULL; +} + +static void getPerUnitString( + const QuantityFormatter &formatter, + UnicodeString &result) { + result = formatter.getByVariant("one")->getPatternWithNoPlaceholders(); + result.trim(); +} + +int32_t MeasureFormat::withPerUnit( + const UnicodeString &formatted, + const MeasureUnit &perUnit, + UnicodeString &appendTo, + UErrorCode &status) const { + int32_t offset = -1; + if (U_FAILURE(status)) { + return offset; + } + const SimplePatternFormatter *perUnitFormatter = getPerUnitFormatter( + perUnit.getIndex(), widthToIndex(width)); + if (perUnitFormatter != NULL) { + const UnicodeString *params[] = {&formatted}; + perUnitFormatter->format( + params, + UPRV_LENGTHOF(params), + appendTo, + &offset, + 1, + status); + return offset; + } + const SimplePatternFormatter *perFormatter = getPerFormatter( + widthToIndex(width), status); + const QuantityFormatter *qf = getQuantityFormatter( + perUnit.getIndex(), widthToIndex(width), status); + if (U_FAILURE(status)) { + return offset; + } + UnicodeString perUnitString; + getPerUnitString(*qf, perUnitString); + const UnicodeString *params[] = {&formatted, &perUnitString}; + perFormatter->format( + params, + UPRV_LENGTHOF(params), + appendTo, + &offset, + 1, + status); + return offset; +} + UnicodeString &MeasureFormat::formatMeasuresSlowTrack( const Measure *measures, int32_t measureCount, diff --git a/icu4c/source/i18n/quantityformatter.cpp b/icu4c/source/i18n/quantityformatter.cpp index 8bec7a97346..bef509a7186 100644 --- a/icu4c/source/i18n/quantityformatter.cpp +++ b/icu4c/source/i18n/quantityformatter.cpp @@ -112,6 +112,19 @@ UBool QuantityFormatter::isValid() const { return formatters[0] != NULL; } +const SimplePatternFormatter *QuantityFormatter::getByVariant( + const char *variant) const { + int32_t pluralIndex = getPluralIndex(variant); + if (pluralIndex == -1) { + pluralIndex = 0; + } + const SimplePatternFormatter *pattern = formatters[pluralIndex]; + if (pattern == NULL) { + pattern = formatters[0]; + } + return pattern; +} + UnicodeString &QuantityFormatter::format( const Formattable& quantity, const NumberFormat &fmt, @@ -147,14 +160,7 @@ UnicodeString &QuantityFormatter::format( if (U_FAILURE(status)) { return appendTo; } - int32_t pluralIndex = getPluralIndex(buffer.data()); - if (pluralIndex == -1) { - pluralIndex = 0; - } - const SimplePatternFormatter *pattern = formatters[pluralIndex]; - if (pattern == NULL) { - pattern = formatters[0]; - } + const SimplePatternFormatter *pattern = getByVariant(buffer.data()); if (pattern == NULL) { status = U_INVALID_STATE_ERROR; return appendTo; diff --git a/icu4c/source/i18n/quantityformatter.h b/icu4c/source/i18n/quantityformatter.h index 5dceaf652dc..b71a1760760 100644 --- a/icu4c/source/i18n/quantityformatter.h +++ b/icu4c/source/i18n/quantityformatter.h @@ -80,6 +80,13 @@ public: */ UBool isValid() const; + /** + * Gets the pattern formatter that would be used for a particular variant. + * If isValid() returns TRUE, this method is guaranteed to return a + * non-NULL value. + */ + const SimplePatternFormatter *getByVariant(const char *variant) const; + /** * Formats a quantity with this object appending the result to appendTo. * At least the "other" variant must be added to this object for this diff --git a/icu4c/source/i18n/unicode/measfmt.h b/icu4c/source/i18n/unicode/measfmt.h index 188aefdf840..4bbc27d7dcd 100644 --- a/icu4c/source/i18n/unicode/measfmt.h +++ b/icu4c/source/i18n/unicode/measfmt.h @@ -73,12 +73,14 @@ typedef enum UMeasureFormatWidth UMeasureFormatWidth; U_NAMESPACE_BEGIN class Measure; +class MeasureUnit; class NumberFormat; class PluralRules; class MeasureFormatCacheData; class SharedNumberFormat; class SharedPluralRules; class QuantityFormatter; +class SimplePatternFormatter; class ListFormatter; class DateFormat; @@ -190,6 +192,29 @@ class U_I18N_API MeasureFormat : public Format { UErrorCode &status) const; #endif /* U_HIDE_DRAFT_API */ +#ifndef U_HIDE_INTERNAL_API + /** + * Works like formatMeasures but adds a per unit. An example of such a + * formatted string is 3 meters, 3.5 centimeters per second. + * @param measures array of measure objects. + * @param measureCount the number of measure objects. + * @param perUnit The per unit. In the example formatted string, + * it is *MeasureUnit::createSecond(status). + * @param appendTo formatted string appended here. + * @param pos the field position. + * @param status the error. + * @return appendTo reference + * + * @internal + */ + UnicodeString &formatMeasuresPer( + const Measure *measures, + int32_t measureCount, + const MeasureUnit &perUnit, + UnicodeString &appendTo, + FieldPosition &pos, + UErrorCode &status) const; +#endif /* U_HIDE_INTERNAL_API */ /** * Return a formatter for CurrencyAmount objects in the given @@ -318,6 +343,20 @@ class U_I18N_API MeasureFormat : public Format { int32_t widthIndex, UErrorCode &status) const; + const SimplePatternFormatter *getPerUnitFormatter( + int32_t index, + int32_t widthIndex) const; + + const SimplePatternFormatter *getPerFormatter( + int32_t widthIndex, + UErrorCode &status) const; + + int32_t withPerUnit( + const UnicodeString &formatted, + const MeasureUnit &perUnit, + UnicodeString &appendTo, + UErrorCode &status) const; + UnicodeString &formatMeasure( const Measure &measure, const NumberFormat &nf, diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp index 5b1cbcaa95a..221d6416634 100644 --- a/icu4c/source/test/intltest/measfmttest.cpp +++ b/icu4c/source/test/intltest/measfmttest.cpp @@ -47,11 +47,13 @@ private: void TestGreek(); void TestFormatSingleArg(); void TestFormatMeasuresZeroArg(); + void TestMultiplesWithPer(); void TestMultiples(); void TestGram(); void TestCurrencies(); void TestFieldPosition(); void TestFieldPositionMultiple(); + void TestFieldPositionMultipleWithPer(); void TestBadArg(); void TestEquality(); void TestGroupingSeparator(); @@ -74,6 +76,11 @@ private: const MeasureFormat &fmt, const ExpectedResult *expectedResults, int32_t count); + void helperTestMultiplesWithPer( + const Locale &locale, + UMeasureFormatWidth width, + const MeasureUnit &unit, + const char *expected); void helperTestMultiples( const Locale &locale, UMeasureFormatWidth width, @@ -87,6 +94,16 @@ private: NumberFormat::EAlignmentFields field, int32_t start, int32_t end); + void verifyFieldPositionWithPer( + const char *description, + const MeasureFormat &fmt, + const UnicodeString &prefix, + const Measure *measures, + int32_t measureCount, + const MeasureUnit &perUnit, + NumberFormat::EAlignmentFields field, + int32_t start, + int32_t end); }; void MeasureFormatTest::runIndexedTest( @@ -105,11 +122,13 @@ void MeasureFormatTest::runIndexedTest( TESTCASE_AUTO(TestGreek); TESTCASE_AUTO(TestFormatSingleArg); TESTCASE_AUTO(TestFormatMeasuresZeroArg); + TESTCASE_AUTO(TestMultiplesWithPer); TESTCASE_AUTO(TestMultiples); TESTCASE_AUTO(TestGram); TESTCASE_AUTO(TestCurrencies); TESTCASE_AUTO(TestFieldPosition); TESTCASE_AUTO(TestFieldPositionMultiple); + TESTCASE_AUTO(TestFieldPositionMultipleWithPer); TESTCASE_AUTO(TestBadArg); TESTCASE_AUTO(TestEquality); TESTCASE_AUTO(TestGroupingSeparator); @@ -841,6 +860,67 @@ void MeasureFormatTest::TestFormatMeasuresZeroArg() { verifyFormat("TestFormatMeasuresZeroArg", fmt, NULL, 0, ""); } +void MeasureFormatTest::TestMultiplesWithPer() { + Locale en("en"); + UErrorCode status = U_ZERO_ERROR; + LocalPointer second(MeasureUnit::createSecond(status)); + LocalPointer minute(MeasureUnit::createMinute(status)); + if (!assertSuccess("", status)) { + return; + } + + // Per unit test. + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_WIDE, *second, "2 miles, 1 foot, 2.3 inches per second"); + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_SHORT, *second, "2 mi, 1 ft, 2.3 inps"); + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_NARROW, *second, "2mi 1\\u2032 2.3\\u2033/s"); + + // Fallback compound per test + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_WIDE, *minute, "2 miles, 1 foot, 2.3 inches per minute"); + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_SHORT, *minute, "2 mi, 1 ft, 2.3 in/min"); + helperTestMultiplesWithPer( + en, UMEASFMT_WIDTH_NARROW, *minute, "2mi 1\\u2032 2.3\\u2033/m"); +} + +void MeasureFormatTest::helperTestMultiplesWithPer( + const Locale &locale, + UMeasureFormatWidth width, + const MeasureUnit &perUnit, + const char *expected) { + UErrorCode status = U_ZERO_ERROR; + FieldPosition pos(0); + MeasureFormat fmt(locale, width, status); + if (!assertSuccess("Error creating format object", status)) { + return; + } + Measure measures[] = { + Measure(2, MeasureUnit::createMile(status), status), + Measure(1, MeasureUnit::createFoot(status), status), + Measure(2.3, MeasureUnit::createInch(status), status)}; + if (!assertSuccess("Error creating measures", status)) { + return; + } + UnicodeString buffer; + fmt.formatMeasuresPer( + measures, + UPRV_LENGTHOF(measures), + perUnit, + buffer, + pos, + status); + if (!assertSuccess("Error formatting measures with per", status)) { + return; + } + assertEquals( + "TestMultiplesWithPer", + UnicodeString(expected).unescape(), + buffer); +} + void MeasureFormatTest::TestMultiples() { Locale ru("ru"); Locale en("en"); @@ -1017,6 +1097,99 @@ void MeasureFormatTest::TestFieldPositionMultiple() { 0); } +void MeasureFormatTest::TestFieldPositionMultipleWithPer() { + UErrorCode status = U_ZERO_ERROR; + MeasureFormat fmt("en", UMEASFMT_WIDTH_SHORT, status); + if (!assertSuccess("Error creating format object", status)) { + return; + } + Measure first[] = { + Measure(354, MeasureUnit::createMeter(status), status), + Measure(23, MeasureUnit::createCentimeter(status), status)}; + Measure second[] = { + Measure(354, MeasureUnit::createMeter(status), status), + Measure(23, MeasureUnit::createCentimeter(status), status), + Measure(5.4, MeasureUnit::createMillimeter(status), status)}; + Measure third[] = { + Measure(3, MeasureUnit::createMeter(status), status), + Measure(23, MeasureUnit::createCentimeter(status), status), + Measure(5, MeasureUnit::createMillimeter(status), status)}; + if (!assertSuccess("Error creating measure objects", status)) { + return; + } + UnicodeString prefix("123456: "); + + LocalPointer secondUnit(MeasureUnit::createSecond(status)); + LocalPointer minuteUnit(MeasureUnit::createMinute(status)); + if (!assertSuccess("Error creating format object", status)) { + return; + } + + // per unit test + verifyFieldPositionWithPer( + "Integer", + fmt, + prefix, + first, + UPRV_LENGTHOF(first), + *secondUnit, + NumberFormat::kIntegerField, + 8, + 11); + verifyFieldPositionWithPer( + "Decimal separator", + fmt, + prefix, + second, + UPRV_LENGTHOF(second), + *secondUnit, + NumberFormat::kDecimalSeparatorField, + 23, + 24); + verifyFieldPositionWithPer( + "no decimal separator", + fmt, + prefix, + third, + UPRV_LENGTHOF(third), + *secondUnit, + NumberFormat::kDecimalSeparatorField, + 0, + 0); + + // Fallback to compound per test + verifyFieldPositionWithPer( + "Integer", + fmt, + prefix, + first, + UPRV_LENGTHOF(first), + *minuteUnit, + NumberFormat::kIntegerField, + 8, + 11); + verifyFieldPositionWithPer( + "Decimal separator", + fmt, + prefix, + second, + UPRV_LENGTHOF(second), + *minuteUnit, + NumberFormat::kDecimalSeparatorField, + 23, + 24); + verifyFieldPositionWithPer( + "no decimal separator", + fmt, + prefix, + third, + UPRV_LENGTHOF(third), + *minuteUnit, + NumberFormat::kDecimalSeparatorField, + 0, + 0); +} + void MeasureFormatTest::TestBadArg() { UErrorCode status = U_ZERO_ERROR; MeasureFormat fmt("en", UMEASFMT_WIDTH_SHORT, status); @@ -1150,6 +1323,40 @@ void MeasureFormatTest::verifyFieldPosition( assertEquals(endIndex.data(), end, pos.getEndIndex()); } +void MeasureFormatTest::verifyFieldPositionWithPer( + const char *description, + const MeasureFormat &fmt, + const UnicodeString &prefix, + const Measure *measures, + int32_t measureCount, + const MeasureUnit &perUnit, + NumberFormat::EAlignmentFields field, + int32_t start, + int32_t end) { + UnicodeString result(prefix); + FieldPosition pos(field); + UErrorCode status = U_ZERO_ERROR; + CharString ch; + const char *descPrefix = ch.append(description, status) + .append(": ", status).data(); + CharString beginIndex; + beginIndex.append(descPrefix, status).append("beginIndex", status); + CharString endIndex; + endIndex.append(descPrefix, status).append("endIndex", status); + fmt.formatMeasuresPer( + measures, + measureCount, + perUnit, + result, + pos, + status); + if (!assertSuccess("Error formatting", status)) { + return; + } + assertEquals(beginIndex.data(), start, pos.getBeginIndex()); + assertEquals(endIndex.data(), end, pos.getEndIndex()); +} + void MeasureFormatTest::verifyFormat( const char *description, const MeasureFormat &fmt,