Skip to content

Commit

Permalink
ICU-22781 Support Arbitrary Constant Unit Formatting (Java)
Browse files Browse the repository at this point in the history
- Added support for constant denominators in MeasureUnit and LongNameHandler
- Implemented test cases for formatting units with arbitrary constant denominators
- Updated MeasureUnit serialization and product methods to handle constant denominators
- Added comprehensive test coverage for complex unit formatting scenarios
  • Loading branch information
younies committed Feb 6, 2025
1 parent ddabf0f commit f495d10
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand All @@ -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())
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down

0 comments on commit f495d10

Please sign in to comment.