From f30956fc9c9fd9e83189ac1c8b372dea0832eb90 Mon Sep 17 00:00:00 2001 From: Younies Date: Tue, 15 Mar 2022 20:29:45 +0000 Subject: [PATCH] ICU-21840 Fix formatting Aliases See #2016 --- icu4c/source/i18n/number_longnames.cpp | 28 +++++++++++-- icu4c/source/test/intltest/numbertest.h | 3 +- icu4c/source/test/intltest/numbertest_api.cpp | 32 +++++++++++++++ .../ibm/icu/impl/number/LongNameHandler.java | 32 +++++++++++++-- .../test/number/NumberFormatterApiTest.java | 39 +++++++++++++++++++ 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp index 5a4cf6321c8..b4e96504ded 100644 --- a/icu4c/source/i18n/number_longnames.cpp +++ b/icu4c/source/i18n/number_longnames.cpp @@ -431,13 +431,33 @@ void getMeasureData(const Locale &locale, subKey.append(unit.getType(), status); subKey.append("/", status); + // Check if unitSubType is an alias or not. + LocalUResourceBundlePointer aliasBundle(ures_open(U_ICUDATA_ALIAS, "metadata", &status)); + + UErrorCode aliasStatus = status; + StackUResourceBundle aliasFillIn; + CharString aliasKey; + aliasKey.append("alias/unit/", aliasStatus); + aliasKey.append(unit.getSubtype(), aliasStatus); + aliasKey.append("/replacement", aliasStatus); + ures_getByKeyWithFallback(aliasBundle.getAlias(), aliasKey.data(), aliasFillIn.getAlias(), + &aliasStatus); + CharString unitSubType; + if (!U_FAILURE(aliasStatus)) { + // This means the subType is an alias. Then, replace unitSubType with the replacement. + auto replacement = ures_getUnicodeString(aliasFillIn.getAlias(), &status); + unitSubType.appendInvariantChars(replacement, status); + } else { + unitSubType.append(unit.getSubtype(), status); + } + // Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ... // TODO(ICU-20400): Get duration-*-person data properly with aliases. - int32_t subtypeLen = static_cast(uprv_strlen(unit.getSubtype())); - if (subtypeLen > 7 && uprv_strcmp(unit.getSubtype() + subtypeLen - 7, "-person") == 0) { - subKey.append({unit.getSubtype(), subtypeLen - 7}, status); + int32_t subtypeLen = static_cast(uprv_strlen(unitSubType.data())); + if (subtypeLen > 7 && uprv_strcmp(unitSubType.data() + subtypeLen - 7, "-person") == 0) { + subKey.append({unitSubType.data(), subtypeLen - 7}, status); } else { - subKey.append({unit.getSubtype(), subtypeLen}, status); + subKey.append({unitSubType.data(), subtypeLen}, status); } if (width != UNUM_UNIT_WIDTH_FULL_NAME) { diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 7556786c502..c0f2e6fd58a 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -101,7 +101,8 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition { void toObject(); void toDecimalNumber(); void microPropsInternals(); - + void formatUnitsAliases(); + void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0) override; private: diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index a8f7326792e..b477a16e93e 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -129,6 +129,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha TESTCASE_AUTO(toObject); TESTCASE_AUTO(toDecimalNumber); TESTCASE_AUTO(microPropsInternals); + TESTCASE_AUTO(formatUnitsAliases); TESTCASE_AUTO_END; } @@ -6174,6 +6175,37 @@ void NumberFormatterApiTest::microPropsInternals() { assertEquals("Copy Assigned capacity", 4, copyAssigned.mixedMeasures.getCapacity()); } +void NumberFormatterApiTest::formatUnitsAliases() { + IcuTestErrorCode status(*this, "formatUnitsAliases"); + + struct TestCase { + const MeasureUnit measureUnit; + const UnicodeString expectedFormat; + } testCases[]{ + // Aliases + {MeasureUnit::getMilligramPerDeciliter(), u"2 milligrams per deciliter"}, + {MeasureUnit::getLiterPer100Kilometers(), u"2 liters per 100 kilometers"}, + {MeasureUnit::getPartPerMillion(), u"2 parts per million"}, + {MeasureUnit::getMillimeterOfMercury(), u"2 millimeters of mercury"}, + + // Replacements + {MeasureUnit::getMilligramOfglucosePerDeciliter(), u"2 milligrams per deciliter"}, + {MeasureUnit::forIdentifier("millimeter-ofhg", status), u"2 millimeters of mercury"}, + {MeasureUnit::forIdentifier("liter-per-100-kilometer", status), u"2 liters per 100 kilometers"}, + {MeasureUnit::forIdentifier("permillion", status), u"2 parts per million"}, + }; + + for (const auto &testCase : testCases) { + UnicodeString actualFormat = NumberFormatter::withLocale(icu::Locale::getEnglish()) + .unit(testCase.measureUnit) + .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME) + .formatDouble(2.0, status) + .toString(status); + + assertEquals("test unit aliases", testCase.expectedFormat, actualFormat); + } +} + /* For skeleton comparisons: this checks the toSkeleton output for `f` and for * `conciseSkeleton` against the normalized version of `uskeleton` - this does * not round-trip uskeleton itself. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java index 4f5dc01254f..6ccc96e711a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java @@ -329,6 +329,21 @@ public class LongNameHandler } } + private static final class AliasSink extends UResource.Sink { + String replacement; + + @Override + public void put(UResource.Key key, UResource.Value value, boolean noFallback) { + UResource.Table aliasTable = value.getTable(); + for (int i = 0; aliasTable.getKeyAndValue(i, key, value); ++i) { + String keyString = key.toString(); + if (keyString.equals("replacement")) { + this.replacement = value.toString(); + } + } + } + } + // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed. static void getMeasureData( ULocale locale, @@ -346,12 +361,23 @@ public class LongNameHandler subKey.append(unit.getType()); subKey.append("/"); + // If the unit is an alias, replace it is identifier with the replacement. + String unitSubType = unit.getSubtype(); + ICUResourceBundle metadataResource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metadata"); + AliasSink aliasSink = new AliasSink(); + + metadataResource.getAllItemsWithFallbackNoFail("alias/unit/" + unitSubType, aliasSink); + if (aliasSink.replacement != null) { + unitSubType = aliasSink.replacement; + } + + // Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ... // TODO(ICU-20400): Get duration-*-person data properly with aliases. - if (unit.getSubtype() != null && unit.getSubtype().endsWith("-person")) { - subKey.append(unit.getSubtype(), 0, unit.getSubtype().length() - 7); + if (unitSubType != null && unitSubType.endsWith("-person")) { + subKey.append(unitSubType, 0, unitSubType.length() - 7); } else { - subKey.append(unit.getSubtype()); + subKey.append(unitSubType); } if (width != UnitWidth.FULL_NAME) { diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java index 054a5f34d6e..e4a8e9d53db 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java @@ -5857,6 +5857,45 @@ public class NumberFormatterApiTest extends TestFmwk { } } + @Test + public void formatUnitsAliases() { + + class TestCase { + final MeasureUnit measureUnit; + final String expectedFormat; + + TestCase(MeasureUnit measureUnit, String expectedFormat) { + this.measureUnit = measureUnit; + this.expectedFormat = expectedFormat; + } + } + + TestCase[] testCases = { + // Aliases + new TestCase(MeasureUnit.MILLIGRAM_PER_DECILITER, "2 milligrams per deciliter"), + new TestCase(MeasureUnit.LITER_PER_100KILOMETERS, "2 liters per 100 kilometers"), + new TestCase(MeasureUnit.PART_PER_MILLION, "2 parts per million"), + new TestCase(MeasureUnit.MILLIMETER_OF_MERCURY, "2 millimeters of mercury"), + + // Replacements + new TestCase(MeasureUnit.MILLIGRAM_OFGLUCOSE_PER_DECILITER, "2 milligrams per deciliter"), + new TestCase(MeasureUnit.forIdentifier("millimeter-ofhg"), "2 millimeters of mercury"), + new TestCase(MeasureUnit.forIdentifier("liter-per-100-kilometer"), "2 liters per 100 kilometers"), + new TestCase(MeasureUnit.forIdentifier("permillion"), "2 parts per million"), + }; + + for (TestCase testCase : testCases) { + String actualFormat = NumberFormatter + .withLocale(ULocale.ENGLISH) + .unit(testCase.measureUnit) + .unitWidth(UnitWidth.FULL_NAME) + .format(2.0) + .toString(); + + assertEquals("test unit aliases", testCase.expectedFormat, actualFormat); + } + } + static void assertFormatDescending( String message, String skeleton, -- 2.50.1