ICU-22122 Support Locale Tags (ms, mu and rg)

See #2182
This commit is contained in:
Younies Mahmoud 2022-09-20 20:02:48 +00:00 committed by Markus Scherer
parent f5367befba
commit dbfe830108
15 changed files with 362 additions and 68 deletions

View file

@ -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) {
}

View file

@ -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);
if (U_FAILURE(status)) {
outPreferences = nullptr;
preferenceCount = 0;
return;
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<UnitPreference>
U_I18N_API UnitPreferences::getPreferencesFor(StringPiece category, StringPiece usage,
const Locale &locale, UErrorCode &status) const {
MaybeStackVector<UnitPreference> 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)) {
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

View file

@ -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<UnitPreference> 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

View file

@ -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<UnitPreference> 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)) {

View file

@ -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

View file

@ -70,6 +70,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
void unitGender();
void unitNotConvertible();
void unitPercent();
void unitLocaleTags();
void percentParity();
void roundingFraction();
void roundingFigures();

View file

@ -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());

View file

@ -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");
}

View file

@ -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())) {

View file

@ -20,8 +20,7 @@ public class UsagePrefsHandler implements MicroPropsGenerator {
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);
}
/**

View file

@ -4,14 +4,28 @@ package com.ibm.icu.impl.units;
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<String, String> measurementSystem;
static {
Map tempMS = new HashMap<String, String>();
tempMS.put("metric", "001");
tempMS.put("ussystem", "US");
tempMS.put("uksystem", "GB");
measurementSystem = Collections.unmodifiableMap(tempMS);
}
private HashMap<String, HashMap<String, UnitPreference[]>> mapToUnitPreferences = new HashMap<>();
@ -56,7 +70,48 @@ public class UnitPreferences {
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 class UnitPreferences {
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 class UnitPreferences {
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() {

View file

@ -11,6 +11,7 @@ import com.ibm.icu.impl.ICUData;
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 class UnitsData {
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 {

View file

@ -10,6 +10,7 @@ import com.ibm.icu.impl.IllegalIcuArgumentException;
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 @@ import com.ibm.icu.util.MeasureUnit;
* `foot+inch`, otherwise, the output will be in `inch`.
* <p>
* 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<MeasureUnit> outputUnits_ = new ArrayList<>();
private ArrayList<ConverterPreference> 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];

View file

@ -26,7 +26,7 @@ import com.ibm.icu.impl.units.UnitsData;
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 @@ public class UnitsTest {
*/
String category;
String usage;
ULocale locale;
String region;
Pair<String, MeasureUnitImpl> inputUnit;
BigDecimal input;
@ -651,6 +652,7 @@ public class UnitsTest {
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<String, String> output :
@ -667,8 +669,8 @@ public class UnitsTest {
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 class UnitsTest {
}
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<Measure> 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 class UnitsTest {
// 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<Measure> measures = router.route(testCase.input, null).complexConverterResult.measures;
assertEquals("Measures size must be the same as expected units",
@ -785,7 +788,9 @@ public class UnitsTest {
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());

View file

@ -489,6 +489,65 @@ public class NumberFormatterApiTest extends TestFmwk {
"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(