From dbfe83010856a58698edf68476ef693236f49a5a Mon Sep 17 00:00:00 2001 From: Younies Mahmoud Date: Tue, 20 Sep 2022 20:02:48 +0000 Subject: [PATCH] ICU-22122 Support Locale Tags (ms, mu and rg) See #2182 --- icu4c/source/i18n/number_usageprefs.cpp | 2 +- icu4c/source/i18n/units_data.cpp | 103 +++++++++++++++--- icu4c/source/i18n/units_data.h | 16 ++- icu4c/source/i18n/units_router.cpp | 29 +++-- icu4c/source/i18n/units_router.h | 8 +- icu4c/source/test/intltest/numbertest.h | 1 + icu4c/source/test/intltest/numbertest_api.cpp | 85 +++++++++++++++ .../source/test/intltest/units_data_test.cpp | 19 ++-- icu4c/source/test/intltest/units_test.cpp | 8 +- .../icu/impl/number/UsagePrefsHandler.java | 3 +- .../ibm/icu/impl/units/UnitPreferences.java | 62 ++++++++++- .../src/com/ibm/icu/impl/units/UnitsData.java | 5 +- .../com/ibm/icu/impl/units/UnitsRouter.java | 11 +- .../com/ibm/icu/dev/test/impl/UnitsTest.java | 17 ++- .../test/number/NumberFormatterApiTest.java | 59 ++++++++++ 15 files changed, 361 insertions(+), 67 deletions(-) diff --git a/icu4c/source/i18n/number_usageprefs.cpp b/icu4c/source/i18n/number_usageprefs.cpp index 5d93d12cce78..26fdfafeea4e 100644 --- a/icu4c/source/i18n/number_usageprefs.cpp +++ b/icu4c/source/i18n/number_usageprefs.cpp @@ -153,7 +153,7 @@ UsagePrefsHandler::UsagePrefsHandler(const Locale &locale, const StringPiece usage, const MicroPropsGenerator *parent, UErrorCode &status) - : fUnitsRouter(inputUnit, StringPiece(locale.getCountry()), usage, status), + : fUnitsRouter(inputUnit, locale, usage, status), fParent(parent) { } diff --git a/icu4c/source/i18n/units_data.cpp b/icu4c/source/i18n/units_data.cpp index d1d1fc5bc05e..1456af4d0533 100644 --- a/icu4c/source/i18n/units_data.cpp +++ b/icu4c/source/i18n/units_data.cpp @@ -5,10 +5,12 @@ #if !UCONFIG_NO_FORMATTING +#include "bytesinkutil.h" #include "cstring.h" #include "number_decimalquantity.h" #include "resource.h" #include "uassert.h" +#include "unicode/locid.h" #include "unicode/unistr.h" #include "unicode/ures.h" #include "units_data.h" @@ -387,24 +389,97 @@ U_I18N_API UnitPreferences::UnitPreferences(UErrorCode &status) { ures_getAllItemsWithFallback(unitsBundle.getAlias(), "unitPreferenceData", sink, status); } -// TODO: make outPreferences const? -// -// TODO: consider replacing `UnitPreference **&outPreferences` with slice class -// of some kind. -void U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringPiece usage, - StringPiece region, - const UnitPreference *const *&outPreferences, - int32_t &preferenceCount, UErrorCode &status) const { - int32_t idx = getPreferenceMetadataIndex(&metadata_, category, usage, region, status); +CharString getKeyWordValue(const Locale &locale, StringPiece kw, UErrorCode &status) { + CharString result; + if (U_FAILURE(status)) { return result; } + { + CharStringByteSink sink(&result); + locale.getKeywordValue(kw, sink, status); + } + if (U_SUCCESS(status) && result.isEmpty()) { + status = U_MISSING_RESOURCE_ERROR; + } + return result; +} + +MaybeStackVector + U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringPiece usage, + const Locale &locale, UErrorCode &status) const { + + MaybeStackVector result; + + // TODO: remove this once all the categories are allowed. + UErrorCode internalMuStatus = U_ZERO_ERROR; + if (category.compare("temperature") == 0) { + CharString localeUnitCharString = getKeyWordValue(locale, "mu", internalMuStatus); + if (U_SUCCESS(internalMuStatus)) { + // TODO: use the unit category as Java especially when all the categories are allowed.. + if (localeUnitCharString == "celsius" // + || localeUnitCharString == "fahrenheit" // + || localeUnitCharString == "kelvin" // + ) { + UnitPreference unitPref; + unitPref.unit.append(localeUnitCharString, status); + result.emplaceBackAndCheckErrorCode(status, unitPref); + return result; + } + } + } + + CharString region(locale.getCountry(), status); + + // Check the locale system tag, e.g `ms=metric`. + UErrorCode internalMeasureTagStatus = U_ZERO_ERROR; + CharString localeSystem = getKeyWordValue(locale, "measure", internalMeasureTagStatus); + bool isLocaleSystem = false; + if (U_SUCCESS(internalMeasureTagStatus)) { + if (localeSystem == "metric") { + region.clear(); + region.append("001", status); + isLocaleSystem = true; + } else if (localeSystem == "ussystem") { + region.clear(); + region.append("US", status); + isLocaleSystem = true; + } else if (localeSystem == "uksystem") { + region.clear(); + region.append("GB", status); + isLocaleSystem = true; + } + } + + // Check the region tag, e.g. `rg=uszzz`. + if (!isLocaleSystem) { + UErrorCode internalRgTagStatus = U_ZERO_ERROR; + CharString localeRegion = getKeyWordValue(locale, "rg", internalRgTagStatus); + if (U_SUCCESS(internalRgTagStatus) && localeRegion.length() >= 3) { + if (localeRegion == "default") { + region.clear(); + region.append(localeRegion, status); + } else if (localeRegion[0] >= '0' && localeRegion[0] <= '9') { + region.clear(); + region.append(localeRegion.data(), 3, status); + } else { + // Take the first two character and capitalize them. + region.clear(); + region.append(uprv_toupper(localeRegion[0]), status); + region.append(uprv_toupper(localeRegion[1]), status); + } + } + } + + int32_t idx = + getPreferenceMetadataIndex(&metadata_, category, usage, region.toStringPiece(), status); if (U_FAILURE(status)) { - outPreferences = nullptr; - preferenceCount = 0; - return; + return result; } + U_ASSERT(idx >= 0); // Failures should have been taken care of by `status`. const UnitPreferenceMetadata *m = metadata_[idx]; - outPreferences = unitPrefs_.getAlias() + m->prefsOffset; - preferenceCount = m->prefsCount; + for (int32_t i = 0; i < m->prefsCount; i++) { + result.emplaceBackAndCheckErrorCode(status, *(unitPrefs_[i + m->prefsOffset])); + } + return result; } } // namespace units diff --git a/icu4c/source/i18n/units_data.h b/icu4c/source/i18n/units_data.h index 2c19b9434bd0..118458ecca2b 100644 --- a/icu4c/source/i18n/units_data.h +++ b/icu4c/source/i18n/units_data.h @@ -99,6 +99,13 @@ struct U_I18N_API UnitPreference : public UMemory { CharString unit; double geq; UnicodeString skeleton; + + UnitPreference(const UnitPreference &other) { + UErrorCode status = U_ZERO_ERROR; + this->unit.append(other.unit, status); + this->geq = other.geq; + this->skeleton = other.skeleton; + } }; /** @@ -189,12 +196,11 @@ class U_I18N_API UnitPreferences { * @param preferenceCount The number of unit preferences that belong to the * result set. * @param status Receives status. - * - * TODO(hugovdm): maybe replace `UnitPreference **&outPreferences` with a slice class? */ - void getPreferencesFor(StringPiece category, StringPiece usage, StringPiece region, - const UnitPreference *const *&outPreferences, int32_t &preferenceCount, - UErrorCode &status) const; + MaybeStackVector getPreferencesFor(StringPiece category, StringPiece usage, + const Locale &locale, + + UErrorCode &status) const; protected: // Metadata about the sets of preferences, this is the index for looking up diff --git a/icu4c/source/i18n/units_router.cpp b/icu4c/source/i18n/units_router.cpp index 0e6082fae5c4..03c9b4d1d7c4 100644 --- a/icu4c/source/i18n/units_router.cpp +++ b/icu4c/source/i18n/units_router.cpp @@ -43,17 +43,17 @@ Precision UnitsRouter::parseSkeletonToPrecision(icu::UnicodeString precisionSkel return result; } -UnitsRouter::UnitsRouter(StringPiece inputUnitIdentifier, StringPiece region, StringPiece usage, +UnitsRouter::UnitsRouter(StringPiece inputUnitIdentifier, const Locale &locale, StringPiece usage, UErrorCode &status) { - this->init(MeasureUnit::forIdentifier(inputUnitIdentifier, status), region, usage, status); + this->init(MeasureUnit::forIdentifier(inputUnitIdentifier, status), locale, usage, status); } -UnitsRouter::UnitsRouter(const MeasureUnit &inputUnit, StringPiece region, StringPiece usage, +UnitsRouter::UnitsRouter(const MeasureUnit &inputUnit, const Locale &locale, StringPiece usage, UErrorCode &status) { - this->init(std::move(inputUnit), region, usage, status); + this->init(std::move(inputUnit), locale, usage, status); } -void UnitsRouter::init(const MeasureUnit &inputUnit, StringPiece region, StringPiece usage, +void UnitsRouter::init(const MeasureUnit &inputUnit, const Locale &locale, StringPiece usage, UErrorCode &status) { if (U_FAILURE(status)) { @@ -73,22 +73,19 @@ void UnitsRouter::init(const MeasureUnit &inputUnit, StringPiece region, StringP return; } - const UnitPreference *const *unitPreferences; - int32_t preferencesCount = 0; - prefs.getPreferencesFor(category.toStringPiece(), usage, region, unitPreferences, preferencesCount, - status); - - for (int i = 0; i < preferencesCount; ++i) { - U_ASSERT(unitPreferences[i] != nullptr); - const auto &preference = *unitPreferences[i]; + const MaybeStackVector unitPrefs = + prefs.getPreferencesFor(category.toStringPiece(), usage, locale, status); + for (int32_t i = 0, n = unitPrefs.length(); i < n; ++i) { + U_ASSERT(unitPrefs[i] != nullptr); + const auto preference = unitPrefs[i]; MeasureUnitImpl complexTargetUnitImpl = - MeasureUnitImpl::forIdentifier(preference.unit.data(), status); + MeasureUnitImpl::forIdentifier(preference->unit.data(), status); if (U_FAILURE(status)) { return; } - UnicodeString precision = preference.skeleton; + UnicodeString precision = preference->skeleton; // For now, we only have "precision-increment" in Units Preferences skeleton. // Therefore, we check if the skeleton starts with "precision-increment" and force the program to @@ -103,7 +100,7 @@ void UnitsRouter::init(const MeasureUnit &inputUnit, StringPiece region, StringP outputUnits_.emplaceBackAndCheckErrorCode(status, complexTargetUnitImpl.copy(status).build(status)); converterPreferences_.emplaceBackAndCheckErrorCode(status, inputUnitImpl, complexTargetUnitImpl, - preference.geq, std::move(precision), + preference->geq, std::move(precision), conversionRates, status); if (U_FAILURE(status)) { diff --git a/icu4c/source/i18n/units_router.h b/icu4c/source/i18n/units_router.h index d9fcffb2aa9e..978fdf91fd5b 100644 --- a/icu4c/source/i18n/units_router.h +++ b/icu4c/source/i18n/units_router.h @@ -11,6 +11,7 @@ #include "cmemory.h" #include "measunit_impl.h" +#include "unicode/locid.h" #include "unicode/measunit.h" #include "unicode/stringpiece.h" #include "unicode/uobject.h" @@ -118,9 +119,10 @@ namespace units { */ class U_I18N_API UnitsRouter { public: - UnitsRouter(StringPiece inputUnitIdentifier, StringPiece locale, StringPiece usage, + UnitsRouter(StringPiece inputUnitIdentifier, const Locale &locale, StringPiece usage, + UErrorCode &status); + UnitsRouter(const MeasureUnit &inputUnit, const Locale &locale, StringPiece usage, UErrorCode &status); - UnitsRouter(const MeasureUnit &inputUnit, StringPiece locale, StringPiece usage, UErrorCode &status); /** * Performs locale and usage sensitive unit conversion. @@ -153,7 +155,7 @@ class U_I18N_API UnitsRouter { static number::Precision parseSkeletonToPrecision(icu::UnicodeString precisionSkeleton, UErrorCode &status); - void init(const MeasureUnit &inputUnit, StringPiece locale, StringPiece usage, UErrorCode &status); + void init(const MeasureUnit &inputUnit, const Locale &locale, StringPiece usage, UErrorCode &status); }; } // namespace units diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 25af549eee60..f1d3b50a2a42 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -70,6 +70,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition { void unitGender(); void unitNotConvertible(); void unitPercent(); + void unitLocaleTags(); void percentParity(); void roundingFraction(); void roundingFigures(); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 4cddd61eb665..18ceb194f39d 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -93,6 +93,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha TESTCASE_AUTO(unitNounClass); TESTCASE_AUTO(unitNotConvertible); TESTCASE_AUTO(unitPercent); + TESTCASE_AUTO(unitLocaleTags); if (!quick) { // Slow test: run in exhaustive mode only TESTCASE_AUTO(percentParity); @@ -2948,6 +2949,90 @@ void NumberFormatterApiTest::unitPercent() { u"50 meters per percent"); } +void NumberFormatterApiTest::unitLocaleTags() { + IcuTestErrorCode status(*this, "unitLocaleTags"); + + const struct TestCase { + const UnicodeString message; + const char *locale; + const char *inputUnit; + const double inputValue; + const char *usage; + const char *expectedOutputUnit; + const double expectedOutputValue; + const UnicodeString expectedFormattedNumber; + } cases[] = { + // Test without any tag behaviour + {u"Test the locale without any addition and without usage", "en-US", "celsius", 0, nullptr, + "celsius", 0.0, u"0 degrees Celsius"}, + {u"Test the locale without any addition and usage", "en-US", "celsius", 0, "default", + "fahrenheit", 32.0, u"32 degrees Fahrenheit"}, + + // Test the behaviour of the `mu` tag. + {u"Test the locale with mu = celsius and without usage", "en-US-u-mu-celsius", "fahrenheit", 0, + nullptr, "fahrenheit", 0.0, u"0 degrees Fahrenheit"}, + {u"Test the locale with mu = celsius and with usage", "en-US-u-mu-celsius", "fahrenheit", 0, + "default", "celsius", -18.0, u"-18 degrees Celsius"}, + {u"Test the locale with mu = calsius (wrong spelling) and with usage", "en-US-u-mu-calsius", + "fahrenheit", 0, "default", "fahrenheit", 0.0, u"0 degrees Fahrenheit"}, + {u"Test the locale with mu = meter (only temprature units are supported) and with usage", + "en-US-u-mu-meter", "foot", 0, "default", "inch", 0.0, u"0 inches"}, + + // Test the behaviour of the `ms` tag + {u"Test the locale with ms = metric and without usage", "en-US-u-ms-metric", "fahrenheit", 0, + nullptr, "fahrenheit", 0.0, u"0 degrees Fahrenheit"}, + {u"Test the locale with ms = metric and with usage", "en-US-u-ms-metric", "fahrenheit", 0, + "default", "celsius", -18, u"-18 degrees Celsius"}, + {u"Test the locale with ms = Matric (wrong spelling) and with usage", "en-US-u-ms-Matric", + "fahrenheit", 0, "default", "fahrenheit", 0.0, u"0 degrees Fahrenheit"}, + + // Test the behaviour of the `rg` tag + {u"Test the locale with rg = UK and without usage", "en-US-u-rg-ukzzzz", "fahrenheit", 0, + nullptr, "fahrenheit", 0.0, u"0 degrees Fahrenheit"}, + {u"Test the locale with rg = UK and with usage", "en-US-u-rg-ukzzzz", "fahrenheit", 0, "default", + "celsius", -18, u"-18 degrees Celsius"}, + {"Test the locale with mu = fahrenheit and without usage", "en-US-u-mu-fahrenheit", "celsius", 0, + nullptr, "celsius", 0.0, "0 degrees Celsius"}, + {"Test the locale with mu = fahrenheit and with usage", "en-US-u-mu-fahrenheit", "celsius", 0, + "default", "fahrenheit", 32.0, "32 degrees Fahrenheit"}, + {u"Test the locale with rg = UKOI and with usage", "en-US-u-rg-ukoizzzz", "fahrenheit", 0, + "default", "celsius", -18.0, u"-18 degrees Celsius"}, + + // Test the priorities + {u"Test the locale with mu,ms,rg --> mu tag wins", "en-US-u-mu-celsius-ms-ussystem-rg-uszzzz", + "celsius", 0, "default", "celsius", 0.0, u"0 degrees Celsius"}, + {u"Test the locale with ms,rg --> ms tag wins", "en-US-u-ms-metric-rg-uszzzz", "foot", 1, + "default", "centimeter", 30.0, u"30 centimeters"}, + }; + + for (const auto &testCase : cases) { + UnicodeString message = testCase.message; + Locale locale(testCase.locale); + auto inputUnit = MeasureUnit::forIdentifier(testCase.inputUnit, status); + auto inputValue = testCase.inputValue; + auto usage = testCase.usage; + auto expectedOutputUnit = MeasureUnit::forIdentifier(testCase.expectedOutputUnit, status); + UnicodeString expectedFormattedNumber = testCase.expectedFormattedNumber; + + auto nf = NumberFormatter::with() + .locale(locale) // + .unit(inputUnit) // + .unitWidth(UNUM_UNIT_WIDTH_FULL_NAME); // + if (usage != nullptr) { + nf = nf.usage(usage); + } + auto fn = nf.formatDouble(inputValue, status); + if (status.errIfFailureAndReset()) { + continue; + } + + assertEquals(message, fn.toString(status), expectedFormattedNumber); + // TODO: ICU-22154 + // assertEquals(message, fn.getOutputUnit(status).getIdentifier(), + // expectedOutputUnit.getIdentifier()); + } +} + void NumberFormatterApiTest::percentParity() { IcuTestErrorCode status(*this, "percentParity"); UnlocalizedNumberFormatter uNoUnitPercent = NumberFormatter::with().unit(NoUnit::percent()); diff --git a/icu4c/source/test/intltest/units_data_test.cpp b/icu4c/source/test/intltest/units_data_test.cpp index ece7235ca683..19deda84fa7f 100644 --- a/icu4c/source/test/intltest/units_data_test.cpp +++ b/icu4c/source/test/intltest/units_data_test.cpp @@ -5,7 +5,9 @@ #if !UCONFIG_NO_FORMATTING +#include "cstring.h" #include "measunit_impl.h" +#include "unicode/locid.h" #include "units_data.h" #include "intltest.h" @@ -118,8 +120,7 @@ void UnitsDataTest::testGetPreferencesFor() { {"Unknown usage US", "length", "foobar", "US", USLenMax, USLenMin}, {"Unknown usage 001", "length", "foobar", "XX", WorldLenMax, WorldLenMin}, {"Fallback", "length", "person-height-xyzzy", "DE", "centimeter", "centimeter"}, - {"Fallback twice", "length", "person-height-xyzzy-foo", "DE", "centimeter", - "centimeter"}, + {"Fallback twice", "length", "person-height-xyzzy-foo", "DE", "centimeter", "centimeter"}, // Confirming results for some unitPreferencesTest.txt test cases {"001 area", "area", "default", "001", "square-kilometer", "square-centimeter"}, {"GB area", "area", "default", "GB", "square-mile", "square-inch"}, @@ -140,18 +141,20 @@ void UnitsDataTest::testGetPreferencesFor() { for (const auto &t : testCases) { logln(t.name); - const UnitPreference *const *prefs; - int32_t prefsCount; - preferences.getPreferencesFor(t.category, t.usage, t.region, prefs, prefsCount, status); + CharString localeID; + localeID.append("und-", status); // append undefined language. + localeID.append(t.region, status); + Locale locale(localeID.data()); + auto unitPrefs = preferences.getPreferencesFor(t.category, t.usage, locale, status); if (status.errIfFailureAndReset("getPreferencesFor(\"%s\", \"%s\", \"%s\", ...", t.category, t.usage, t.region)) { continue; } - if (prefsCount > 0) { + if (unitPrefs.length() > 0) { assertEquals(UnicodeString(t.name) + " - max unit", t.expectedBiggest, - prefs[0]->unit.data()); + unitPrefs[0]->unit.data()); assertEquals(UnicodeString(t.name) + " - min unit", t.expectedSmallest, - prefs[prefsCount - 1]->unit.data()); + unitPrefs[unitPrefs.length() - 1]->unit.data()); } else { errln(UnicodeString(t.name) + ": failed to find preferences"); } diff --git a/icu4c/source/test/intltest/units_test.cpp b/icu4c/source/test/intltest/units_test.cpp index ca41e3b0d030..931453c05618 100644 --- a/icu4c/source/test/intltest/units_test.cpp +++ b/icu4c/source/test/intltest/units_test.cpp @@ -948,7 +948,11 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie return; } - UnitsRouter router(inputMeasureUnit, region, usage, status); + CharString localeID; + localeID.append("und-", status); // append undefined language. + localeID.append(region, status); + Locale locale(localeID.data()); + UnitsRouter router(inputMeasureUnit, locale, usage, status); if (status.errIfFailureAndReset("UnitsRouter(<%s>, \"%.*s\", \"%.*s\", status)", inputMeasureUnit.getIdentifier(), region.length(), region.data(), usage.length(), usage.data())) { @@ -976,7 +980,7 @@ void unitPreferencesTestDataLineFn(void *context, char *fields[][2], int32_t fie // Test UnitsRouter created with CLDR units identifiers. CharString inputUnitIdentifier(inputUnit, status); - UnitsRouter router2(inputUnitIdentifier.data(), region, usage, status); + UnitsRouter router2(inputUnitIdentifier.data(), locale, usage, status); if (status.errIfFailureAndReset("UnitsRouter2(<%s>, \"%.*s\", \"%.*s\", status)", inputUnitIdentifier.data(), region.length(), region.data(), usage.length(), usage.data())) { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java index 6de6152d08ca..04c6cf65a0a7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/UsagePrefsHandler.java @@ -20,8 +20,7 @@ public UsagePrefsHandler(ULocale locale, MeasureUnit inputUnit, String usage, Mi assert parent != null; this.fParent = parent; - this.fUnitsRouter = - new UnitsRouter(MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier()), locale.getCountry(), usage); + this.fUnitsRouter = new UnitsRouter(MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier()), locale, usage); } /** diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java index 4ef5479961e3..92d7e808c060 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java @@ -4,14 +4,28 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.UResource; +import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; public class UnitPreferences { + private static final Map measurementSystem; + + static { + Map tempMS = new HashMap(); + tempMS.put("metric", "001"); + tempMS.put("ussystem", "US"); + tempMS.put("uksystem", "GB"); + measurementSystem = Collections.unmodifiableMap(tempMS); + } + private HashMap> mapToUnitPreferences = new HashMap<>(); @@ -56,7 +70,48 @@ private static String[] getAllUsages(String usage) { return result.toArray(new String[0]); } - public UnitPreference[] getPreferencesFor(String category, String usage, String region) { + public UnitPreference[] getPreferencesFor(String category, String usage, ULocale locale, UnitsData data) { + // TODO: remove this condition when all the categories are allowed. + if (category.equals("temperature")) { + String localeUnit = locale.getKeywordValue("mu"); + String localeUnitCategory; + try { + localeUnitCategory = localeUnit == null ? null : data.getCategory(MeasureUnitImpl.forIdentifier(localeUnit)); + } catch (Exception e) { + localeUnitCategory = null; + } + + if (localeUnitCategory != null && category.equals(localeUnitCategory)) { + UnitPreference[] preferences = {new UnitPreference(localeUnit, null, null)}; + return preferences; + } + } + + String region = locale.getCountry(); + + // Check the locale system tag, e.g `ms=metric`. + String localeSystem = locale.getKeywordValue("measure"); + boolean isLocaleSystem = false; + if (measurementSystem.containsKey(localeSystem)) { + isLocaleSystem = true; + region = measurementSystem.get(localeSystem); + } + + // Check the region tag, e.g. `rg=uszzz`. + if (!isLocaleSystem) { + String localeRegion = locale.getKeywordValue("rg"); + if (localeRegion != null && localeRegion.length() >= 3) { + if (localeRegion.equals("default")) { + region = localeRegion; + } else if (Character.isDigit(localeRegion.charAt(0))) { + region = localeRegion.substring(0, 3); // e.g. 001 + } else { + // Capitalize the first two character of the region, e.g. ukzzzz or usca + region = localeRegion.substring(0, 2).toUpperCase(Locale.ROOT); + } + } + } + String[] subUsages = getAllUsages(usage); UnitPreference[] result = null; for (String subUsage : @@ -64,6 +119,7 @@ public UnitPreference[] getPreferencesFor(String category, String usage, String result = getUnitPreferences(category, subUsage, region); if (result != null) break; } + // TODO: if a category is missing, we get an assertion failure, or we // return null, causing a NullPointerException. In C++, we return an // U_MISSING_RESOURCE_ERROR error. @@ -101,8 +157,8 @@ public static class UnitPreference { public UnitPreference(String unit, String geq, String skeleton) { this.unit = unit; - this.geq = new BigDecimal(geq); - this.skeleton = skeleton; + this.geq = geq == null ? BigDecimal.valueOf( Double.MIN_VALUE) /* -inf */ : new BigDecimal(geq); + this.skeleton = skeleton == null? "" : skeleton; } public String getUnit() { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java index bd090d2a5506..2a28beaffb06 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java @@ -11,6 +11,7 @@ import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.IllegalIcuArgumentException; import com.ibm.icu.impl.UResource; +import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; /** @@ -109,8 +110,8 @@ public String getCategory(MeasureUnitImpl measureUnit) { return Categories.indexToCategory[index]; } - public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, String region) { - return this.unitPreferences.getPreferencesFor(category, usage, region); + public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, ULocale locale) { + return this.unitPreferences.getPreferencesFor(category, usage, locale, this); } public static class SimpleUnitIdentifiersSink extends UResource.Sink { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java index 0c38fcb0d0d2..aab13c072926 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java @@ -10,6 +10,7 @@ import com.ibm.icu.impl.number.MicroProps; import com.ibm.icu.number.Precision; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; /** * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to @@ -22,7 +23,7 @@ * `foot+inch`, otherwise, the output will be in `inch`. *

* NOTE: - * the output units and the their limits MUST BE in order, for example, if the output units, from the + * the output units and their limits MUST BE in order, for example, if the output units, from the * previous example, are the following: * {`inch` , limit: no value (-inf)} * {`foot+inch`, limit: 3.0} @@ -46,17 +47,17 @@ public class UnitsRouter { private ArrayList outputUnits_ = new ArrayList<>(); private ArrayList converterPreferences_ = new ArrayList<>(); - public UnitsRouter(String inputUnitIdentifier, String region, String usage) { - this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), region, usage); + public UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage) { + this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), locale, usage); } - public UnitsRouter(MeasureUnitImpl inputUnit, String region, String usage) { + public UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage) { // TODO: do we want to pass in ConversionRates and UnitPreferences instead? // of loading in each UnitsRouter instance? (Or make global?) UnitsData data = new UnitsData(); String category = data.getCategory(inputUnit); - UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, region); + UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, locale); for (int i = 0; i < unitPreferences.length; ++i) { UnitPreferences.UnitPreference preference = unitPreferences[i]; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java index 195593e64352..cd18ce73a38e 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java @@ -26,7 +26,7 @@ import com.ibm.icu.impl.units.UnitsRouter; import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; - +import com.ibm.icu.util.ULocale; public class UnitsTest { @@ -613,6 +613,7 @@ class TestCase { */ String category; String usage; + ULocale locale; String region; Pair inputUnit; BigDecimal input; @@ -651,6 +652,7 @@ private void insertData(String category, this.category = category; this.usage = usage; this.region = region; + this.locale = new ULocale("und-" + this.region); this.inputUnit = Pair.of(inputUnitString, MeasureUnitImpl.UnitsParser.parseForIdentifier(inputUnitString)); this.input = new BigDecimal(inputValue); for (Pair output : @@ -667,8 +669,8 @@ public String toString() { outputUnits.add(unit.second); } return "TestCase: " + category + ", " + usage + ", " + region + "; Input: " + input + - " " + inputUnit.first + "; Expected Values: " + expectedInOrder + - ", Expected Units: " + outputUnits; + " " + inputUnit.first + "; Expected Values: " + expectedInOrder + + ", Expected Units: " + outputUnits; } } @@ -686,7 +688,8 @@ public String toString() { } for (TestCase testCase : tests) { - UnitsRouter router = new UnitsRouter(testCase.inputUnit.second, testCase.region, testCase.usage); + UnitsRouter router = new UnitsRouter(testCase.inputUnit.second, testCase.locale, + testCase.usage); List measures = router.route(testCase.input, null).complexConverterResult.measures; assertEquals("For " + testCase.toString() + ", Measures size must be the same as expected units", @@ -707,7 +710,7 @@ public String toString() { // Test UnitsRouter created with CLDR units identifiers. for (TestCase testCase : tests) { - UnitsRouter router = new UnitsRouter(testCase.inputUnit.first, testCase.region, testCase.usage); + UnitsRouter router = new UnitsRouter(testCase.inputUnit.first, testCase.locale, testCase.usage); List measures = router.route(testCase.input, null).complexConverterResult.measures; assertEquals("Measures size must be the same as expected units", @@ -785,7 +788,9 @@ public TestCase(String name, String category, String usage, String region, Strin UnitsData data = new UnitsData(); for (TestCase t : testCases) { - UnitPreferences.UnitPreference prefs[] = data.getPreferencesFor(t.category, t.usage, t.region); + ULocale locale = new ULocale("und-" + t.region); + UnitPreferences.UnitPreference prefs[] = data.getPreferencesFor(t.category, t.usage, + locale); if (prefs.length > 0) { assertEquals(t.name + " - max unit", t.expectedBiggest, prefs[0].getUnit()); assertEquals(t.name + " - min unit", t.expectedSmallest, prefs[prefs.length - 1].getUnit()); 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 5d5a7a71de30..ec9e5462f2b3 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 @@ -489,6 +489,65 @@ public void notationCompact() { "Kun"); } + @Test + public void unitWithLocaleTags() { + String[][] tests = { + // 0-message, 1- locale, 2- input unit, 3- input value, 4- usage, 5- output unit, 6- output value, 7- formatted number. + // Test without any tag behaviour + {"Test the locale without any addition and without usage", "en-US", "celsius", "0", null, "celsius", "0.0", "0 degrees Celsius"}, + {"Test the locale without any addition and usage", "en-US", "celsius", "0", "default", "fahrenheit", "32.0", "32 degrees Fahrenheit"}, + + // Test the behaviour of the `mu` tag. + {"Test the locale with mu = celsius and without usage", "en-US-u-mu-celsius", "fahrenheit", "0", null, "fahrenheit", "0.0", "0 degrees Fahrenheit"}, + {"Test the locale with mu = celsius and with usage", "en-US-u-mu-celsius", "fahrenheit", "0", "default", "celsius", "-18.0", "-18 degrees Celsius"}, + {"Test the locale with mu = calsius (wrong spelling) and with usage", "en-US-u-mu-calsius", "fahrenheit", "0", "default", "fahrenheit", "0.0", "0 degrees Fahrenheit"}, + {"Test the locale with mu = fahrenheit and without usage", "en-US-u-mu-fahrenheit", "celsius", "0", null, "celsius", "0.0", "0 degrees Celsius"}, + {"Test the locale with mu = fahrenheit and with usage", "en-US-u-mu-fahrenheit", "celsius", "0", "default", "fahrenheit", "32.0", "32 degrees Fahrenheit"}, + {"Test the locale with mu = meter (only temprature units are supported) and with usage", "en-US-u-mu-meter", "foot", "0", "default", "foot", "0.0", "0 inches"}, + + // Test the behaviour of the `ms` tag + {"Test the locale with ms = metric and without usage", "en-US-u-ms-metric", "fahrenheit", "0", null, "fahrenheit", "0.0", "0 degrees Fahrenheit"}, + {"Test the locale with ms = metric and with usage", "en-US-u-ms-metric", "fahrenheit", "0", "default", "celsius", "-18", "-18 degrees Celsius"}, + {"Test the locale with ms = Matric (wrong spelling) and with usage", "en-US-u-ms-Matric", "fahrenheit", "0", "default", "fahrenheit", "0.0", "0 degrees Fahrenheit"}, + + // Test the behaviour of the `rg` tag + {"Test the locale with rg = UK and without usage", "en-US-u-rg-ukzzzz", "fahrenheit", "0", null, "fahrenheit", "0.0", "0 degrees Fahrenheit"}, + {"Test the locale with rg = UK and with usage", "en-US-u-rg-ukzzzz", "fahrenheit", "0", "default", "celsius", "-18", "-18 degrees Celsius"}, + {"Test the locale with rg = UKOI and with usage", "en-US-u-rg-ukoizzzz", "fahrenheit", "0", "default", "celsius", "-18" , "-18 degrees Celsius"}, + + // Test the priorities + {"Test the locale with mu,ms,rg --> mu tag wins", "en-US-u-mu-celsius-ms-ussystem-rg-uszzzz", "celsius", "0", "default", "celsius", "0.0", "0 degrees Celsius"}, + {"Test the locale with ms,rg --> ms tag wins", "en-US-u-ms-metric-rg-uszzzz", "foot", "1", "default", "foot", "30.0", "30 centimeters"}, + }; + + int testIndex = 0; + for (String[] test : tests) { + String message = test[0] + ", index = " + testIndex++; + ULocale locale = ULocale.forLanguageTag(test[1]); + MeasureUnit inputUnit = MeasureUnit.forIdentifier(test[2]); + double inputValue = Double.parseDouble(test[3]); + String usage = test[4]; + MeasureUnit expectedOutputUnit = MeasureUnit.forIdentifier(test[5]); + BigDecimal expectedOutputValue = new BigDecimal(test[6]); + String expectedFormattedMessage = test[7]; + + LocalizedNumberFormatter nf = NumberFormatter.with().locale(locale).unit(inputUnit).unitWidth(UnitWidth.FULL_NAME); + if (usage != null) { + nf = nf.usage(usage); + } + + FormattedNumber fn = nf.format(inputValue); + MeasureUnit actualOutputUnit = fn.getOutputUnit(); + BigDecimal actualOutputValue = fn.toBigDecimal(); + String actualFormattedMessage = fn.toString(); + + assertEquals(message, expectedFormattedMessage, actualFormattedMessage); + // TODO: ICU-22154 + // assertEquals(message, expectedOutputUnit, actualOutputUnit); + assertTrue(message, expectedOutputValue.subtract(actualOutputValue).abs().compareTo(BigDecimal.valueOf(0.0001)) <= 0); + } + } + @Test public void unitMeasure() { assertFormatDescending(