diff --git a/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/NumberFormatTest.java b/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/NumberFormatTest.java index 138200357fc5..2e49a0701b18 100644 --- a/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/NumberFormatTest.java +++ b/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/NumberFormatTest.java @@ -51,6 +51,9 @@ import com.ibm.icu.impl.number.PatternStringUtils; import com.ibm.icu.math.BigDecimal; import com.ibm.icu.math.MathContext; +import com.ibm.icu.number.LocalizedNumberFormatter; +import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.text.CompactDecimalFormat; import com.ibm.icu.text.CurrencyPluralInfo; import com.ibm.icu.text.DecimalFormat; @@ -66,6 +69,7 @@ import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; @RunWith(JUnit4.class) @@ -7056,4 +7060,64 @@ public void TestParseWithEmptyCurr() { } } + + @Test + public void TestArbitraryConstantFormatting() { + + class TestData { + String unitIdentifier; + Integer inputValue; + String expectedOutput; + UnitWidth width; + ULocale locale; + + public TestData(String unitIdentifier, Integer inputValue, UnitWidth width, ULocale locale, + String expectedOutput) { + this.unitIdentifier = unitIdentifier; + this.inputValue = inputValue; + this.expectedOutput = expectedOutput; + this.width = width; + this.locale = locale; + } + } + + TestData[] testData = { + new TestData("meter-per-kelvin-second", 2, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "2 meters per second-kelvin"), + new TestData("meter-per-100-kelvin-second", 3, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "3 meters per 100-second-kelvin"), + new TestData("meter-per-kelvin-second", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter per second-kelvin"), + new TestData("meter-per-1000", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, "1 meter per 1000"), + new TestData("meter-per-1000-second", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter per 1000-second"), + new TestData("meter-per-1000-second-kelvin", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter per 1000-second-kelvin"), + new TestData("meter-per-1-second-kelvin-per-kilogram", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter per 1-kilogram-second-kelvin"), + new TestData("meter-second-per-kilogram-kelvin", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter-second per kilogram-kelvin"), + new TestData("meter-second-per-1000-kilogram-kelvin", 1, UnitWidth.FULL_NAME, ULocale.ENGLISH, + "1 meter-second per 1000-kilogram-kelvin"), + new TestData("meter-second-per-1000-kilogram-kelvin", 1, UnitWidth.SHORT, ULocale.ENGLISH, + "1 m⋅sec/1000⋅kg⋅K"), + new TestData("meter-second-per-1000-kilogram-kelvin", 1, UnitWidth.FULL_NAME, ULocale.GERMAN, + "1 Meter⋅Sekunde pro 1000⋅Kilogramm⋅Kelvin"), + new TestData("meter-second-per-1000-kilogram-kelvin", 1, UnitWidth.SHORT, ULocale.GERMAN, + "1 m⋅Sek./1000⋅kg⋅K"), + }; + + for (TestData testCase : testData) { + MeasureUnit unit = MeasureUnit.forIdentifier(testCase.unitIdentifier); + LocalizedNumberFormatter formatter = NumberFormatter.withLocale(testCase.locale).unit(unit) + .unitWidth(testCase.width); + + String formatted = formatter.format(testCase.inputValue).toString(); + assertEquals( + "Unit: " + testCase.unitIdentifier + ", Width: " + testCase.width + ", Input: " + + testCase.inputValue, + testCase.expectedOutput, formatted); + } + + } } diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/number/LongNameHandler.java index a53166d24820..f757cd686477 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/number/LongNameHandler.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/number/LongNameHandler.java @@ -40,6 +40,7 @@ public class LongNameHandler private static final int DNAM_INDEX = StandardPlural.COUNT + i++; private static final int PER_INDEX = StandardPlural.COUNT + i++; private static final int GENDER_INDEX = StandardPlural.COUNT + i++; + private static final int CONSTANT_DENOMINATOR_INDEX = StandardPlural.COUNT + i++; static final int ARRAY_LENGTH = StandardPlural.COUNT + i++; // Returns the array index that corresponds to the given pluralKeyword. @@ -860,6 +861,13 @@ private static LongNameHandler forArbitraryUnit(ULocale loc, MeasureUnitImpl fullUnit = unit.getCopyOfMeasureUnitImpl(); unit = null; MeasureUnit perUnit = null; + + if (fullUnit.getConstantDenominator() != 0) { + MeasureUnitImpl perUnitImpl = new MeasureUnitImpl(); + perUnitImpl.setConstantDenominator(fullUnit.getConstantDenominator()); + perUnit = perUnitImpl.build(); + } + // TODO(icu-units#28): lots of inefficiency in the handling of // MeasureUnit/MeasureUnitImpl: for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) { @@ -1053,7 +1061,7 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, // TODO(icu-units#28): ensure we have unit tests that change/fail if we // assign incorrect case variants here: if (singleUnitIndex < singleUnits.size() - 1) { - // 4.1. If hasMultiple + // 4.1. If hasMultipleUnits is true singlePluralCategory = derivedTimesPlurals.value0(pluralCategory); singleCaseVariant = derivedTimesCases.value0(caseVariant); pluralCategory = derivedTimesPlurals.value1(pluralCategory); @@ -1116,7 +1124,7 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, String prefixPattern = ""; if (prefix != MeasurePrefix.ONE) { // 4.4.1. set siPrefixPattern to be getValue(that si_prefix, locale, - // length), such as "centy{0}" + // length), such as "centy{0}" StringBuilder prefixKey = new StringBuilder(); // prefixKey looks like "1024p3" or "10p-2": prefixKey.append(prefix.getBase()); @@ -1143,7 +1151,7 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, } // 4.5. Set corePattern to be the getValue(singleUnit, locale, length, - // singlePluralCategory, singleCaseVariant), such as "{0} metrem" + // singlePluralCategory, singleCaseVariant), such as "{0} metrem" String[] singleUnitArray = new String[ARRAY_LENGTH]; // At this point we are left with a Simple Unit: assert singleUnit.build().getIdentifier().equals(singleUnit.getSimpleUnitID()) @@ -1238,8 +1246,9 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, String prefixCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(prefixPattern, sb, 1, 1); - // 4.9.1. Set coreUnit to be the combineLowercasing(locale, length, siPrefixPattern, - // coreUnit) + // 4.9.1. Set coreUnit to be the combineLowercasing(locale, length, + // siPrefixPattern, + // coreUnit) // combineLowercasing(locale, length, prefixPattern, coreUnit) // // TODO(icu-units#28): run this only if prefixPattern does not @@ -1257,7 +1266,7 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, getWithPlural(dimensionalityPrefixPatterns, plural), sb, 1, 1); // 4.10.1. Set coreUnit to be the combineLowercasing(locale, length, - // dimensionalityPrefixPattern, coreUnit) + // dimensionalityPrefixPattern, coreUnit) // combineLowercasing(locale, length, prefixPattern, coreUnit) // // TODO(icu-units#28): run this only if prefixPattern does not @@ -1280,6 +1289,39 @@ private static void processPatternTimes(MeasureUnitImpl productUnit, } } } + + // 5. Handling constant denominator if it exists. + if (productUnit.getConstantDenominator() != 0) { + outArray[CONSTANT_DENOMINATOR_INDEX] = String.valueOf(productUnit.getConstantDenominator()); + Integer pluralIndex = null; + for (StandardPlural plural_ : StandardPlural.values()) { + if (outArray[plural_.ordinal()] != null) { + pluralIndex = plural_.ordinal(); + break; + } + } + + assert pluralIndex != null : "No plural form found for constant denominator"; + + // TODO(ICU-23039): + // Improve the handling of constant_denominator representation. + // For instance, a constant_denominator of 1000000 should be adaptable to + // formats like + // 1,000,000, 1e6, or 1 million. + // Furthermore, ensure consistent pluralization rules for units. For example, + // "meter per 100 seconds" should be evaluated for correct singular/plural + // usage: "second" or "seconds"? + // Similarly, "kilogram per 1000 meters" should be checked for "meter" or + // "meters"? + if (outArray[pluralIndex].length() == 0) { + outArray[pluralIndex] = outArray[CONSTANT_DENOMINATOR_INDEX]; + } else { + outArray[pluralIndex] = SimpleFormatterImpl.formatCompiledPattern( + timesPatternFormatter, outArray[CONSTANT_DENOMINATOR_INDEX], outArray[pluralIndex]); + } + + } + for (StandardPlural plural : StandardPlural.values()) { int pluralIndex = plural.ordinal(); if (globalPlaceholder[pluralIndex] == PlaceholderPosition.BEGINNING) { diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java index 9ae09bebd954..16e78dcaf645 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java @@ -300,7 +300,7 @@ private String getConstantsString(long constantDenominator) { * Normalizes the MeasureUnitImpl and generates the identifier string in place. */ public void serialize() { - if (this.getSingleUnits().size() == 0) { + if (this.getSingleUnits().size() == 0 && this.constantDenominator == 0) { // Dimensionless, constructed by the default constructor: no appending // to this.result, we wish it to contain the zero-length string. return; diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java index 4792770e831d..925a03f436bc 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java @@ -716,6 +716,13 @@ public MeasureUnit product(MeasureUnit other) { implCopy.appendSingleUnit(singleUnit); } + if (this.getConstantDenominator() != 0 && other.getConstantDenominator() != 0) { + throw new UnsupportedOperationException( + "Cannot multiply units that both of them have a constant denominator"); + } + + implCopy.setConstantDenominator(this.getConstantDenominator() + other.getConstantDenominator()); + return implCopy.build(); }