From 9e03b3077c5fac6b79f04a7a531030385a702d36 Mon Sep 17 00:00:00 2001 From: Younies Mahmoud Date: Mon, 20 Jan 2025 11:30:21 +0000 Subject: [PATCH] ICU-22781 Adding support for constant denominators See #3336 --- .../icu/dev/test/format/MeasureUnitTest.java | 87 ++++++- .../ibm/icu/impl/units/MeasureUnitImpl.java | 244 ++++++++++++++---- .../java/com/ibm/icu/util/MeasureUnit.java | 174 ++++++++++--- 3 files changed, 416 insertions(+), 89 deletions(-) diff --git a/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/MeasureUnitTest.java index 01c9295c2235..bc64d401d23c 100644 --- a/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/MeasureUnitTest.java +++ b/icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/format/MeasureUnitTest.java @@ -18,6 +18,7 @@ import java.text.FieldPosition; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -49,6 +50,7 @@ import com.ibm.icu.util.TimeUnit; import com.ibm.icu.util.TimeUnitAmount; import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.MeasureUnit.Complexity; /** * This file contains regular unit tests. @@ -1145,10 +1147,12 @@ public boolean hasSameBehavior(Object a, Object b) } System.out.println("MeasureFormatHandler.hasSameBehavior fails:"); if (!getLocaleEqual) { - System.out.println("- getLocale equality fails: old a1: " + a1.getLocale().getName() + "; test b1: " + b1.getLocale().getName()); + System.out.println("- getLocale equality fails: old a1: " + a1.getLocale().getName() + "; test b1: " + + b1.getLocale().getName()); } if (!getWidthEqual) { - System.out.println("- getWidth equality fails: old a1: " + a1.getWidth().name() + "; test b1: " + b1.getWidth().name()); + System.out.println("- getWidth equality fails: old a1: " + a1.getWidth().name() + "; test b1: " + + b1.getWidth().name()); } if (!numFmtHasSameBehavior) { System.out.println("- getNumberFormat hasSameBehavior fails"); @@ -1311,6 +1315,74 @@ class TestCase { null, MeasureUnit.forIdentifier("")); } + @Test + public void TestAcceptableConstantDenominator() { + class ConstantDenominatorTestCase { + String identifier; + long expectedConstantDenominator; + + ConstantDenominatorTestCase(String identifier, long expectedConstantDenominator) { + this.identifier = identifier; + this.expectedConstantDenominator = expectedConstantDenominator; + } + } + + List testCases = Arrays.asList( + new ConstantDenominatorTestCase("meter-per-1000", 1000), + new ConstantDenominatorTestCase("liter-per-1000-kiloliter", 1000), + new ConstantDenominatorTestCase("liter-per-kilometer", 0), + new ConstantDenominatorTestCase("second-per-1000-minute", 1000), + new ConstantDenominatorTestCase("gram-per-1000-kilogram", 1000), + new ConstantDenominatorTestCase("meter-per-100", 100), + new ConstantDenominatorTestCase("portion-per-1", 1), + new ConstantDenominatorTestCase("portion-per-10", 10), + new ConstantDenominatorTestCase("portion-per-100", 100), + new ConstantDenominatorTestCase("portion-per-1000", 1000), + new ConstantDenominatorTestCase("portion-per-10000", 10000), + new ConstantDenominatorTestCase("portion-per-100000", 100000), + new ConstantDenominatorTestCase("portion-per-1000000", 1000000), + new ConstantDenominatorTestCase("portion-per-10000000", 10000000), + new ConstantDenominatorTestCase("portion-per-100000000", 100000000), + new ConstantDenominatorTestCase("portion-per-1000000000", 1000000000), + new ConstantDenominatorTestCase("portion-per-10000000000", 10000000000L), + new ConstantDenominatorTestCase("portion-per-100000000000", 100000000000L), + new ConstantDenominatorTestCase("portion-per-1000000000000", 1000000000000L), + new ConstantDenominatorTestCase("portion-per-10000000000000", 10000000000000L), + new ConstantDenominatorTestCase("portion-per-100000000000000", 100000000000000L), + new ConstantDenominatorTestCase("portion-per-1000000000000000", 1000000000000000L), + new ConstantDenominatorTestCase("portion-per-10000000000000000", 10000000000000000L), + new ConstantDenominatorTestCase("portion-per-100000000000000000", 100000000000000000L), + new ConstantDenominatorTestCase("portion-per-1000000000000000000", 1000000000000000000L), + new ConstantDenominatorTestCase("portion-per-1e9", 1000000000L), + new ConstantDenominatorTestCase("portion-per-1E9", 1000000000L), + new ConstantDenominatorTestCase("portion-per-10e9", 10000000000L), + new ConstantDenominatorTestCase("portion-per-10E9", 10000000000L), + new ConstantDenominatorTestCase("portion-per-1e10", 10000000000L), + new ConstantDenominatorTestCase("portion-per-1E10", 10000000000L), + new ConstantDenominatorTestCase("portion-per-1e3-kilometer", 1000), + new ConstantDenominatorTestCase("liter-per-12345-kilometer", 12345), + new ConstantDenominatorTestCase("per-1000-kilometer", 1000), + new ConstantDenominatorTestCase("liter-per-1000-kiloliter", 1000), + // NOTE: The following constant denominator should be 0. However, since + // `100-kilometer` is treated as a unit in CLDR, + // the unit does not have a constant denominator. + // This issue should be addressed in CLDR. + new ConstantDenominatorTestCase("meter-per-100-kilometer", 0), + // NOTE: the following CLDR identifier should be invalid, but because + // `100-kilometer` is considered a unit in CLDR, + // one `100` will be considered as a unit constant denominator and the other + // `100` will be considered part of the unit. + // This issue should be addressed in CLDR. + new ConstantDenominatorTestCase("meter-per-100-100-kilometer", 100)); + + for (ConstantDenominatorTestCase testCase : testCases) { + MeasureUnit unit = MeasureUnit.forIdentifier(testCase.identifier); + assertEquals("Constant denominator for " + testCase.identifier, testCase.expectedConstantDenominator, + unit.getConstantDenominator()); + assertEquals("Complexity for " + testCase.identifier, Complexity.COMPOUND, unit.getComplexity()); + } + } + @Test public void TestInvalidIdentifiers() { final String inputs[] = { @@ -1348,6 +1420,17 @@ public void TestInvalidIdentifiers() { // Compound units not supported in mixed units yet. TODO(CLDR-13701). "kilonewton-meter-and-newton-meter", + + // Invalid units because of invalid constant denominator + "per-1000", + "meter-per-1000-1000", + "meter-per-1000-second-1000-kilometer", + "1000-meter", + "meter-1000", + "meter-per-1000-1000", + "meter-per-1000-second-1000-kilometer", + "per-1000-and-per-1000", + }; for (String input : inputs) { 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 2363dee92be3..e42797f20161 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 @@ -14,6 +14,7 @@ import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.StringTrieBuilder; + public class MeasureUnitImpl { /** @@ -24,6 +25,12 @@ public class MeasureUnitImpl { * The complexity, either SINGLE, COMPOUND, or MIXED. */ private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE; + /** + * The constant denominator. + * + * NOTE: when it is 0, it means there is no constant denominator. + */ + private long constantDenominator = 0; /** * The list of single units. These may be summed or multiplied, based on the * value of the complexity field. @@ -67,6 +74,7 @@ public MeasureUnitImpl copy() { MeasureUnitImpl result = new MeasureUnitImpl(); result.complexity = this.complexity; result.identifier = this.identifier; + result.constantDenominator = this.constantDenominator; for (SingleUnitImpl singleUnit : this.singleUnits) { result.singleUnits.add(singleUnit.copy()); } @@ -234,6 +242,20 @@ public void setComplexity(MeasureUnit.Complexity complexity) { this.complexity = complexity; } + /** + * Get the constant denominator. + */ + public long getConstantDenominator() { + return constantDenominator; + } + + /** + * Set the constant denominator. + */ + public void setConstantDenominator(long constantDenominator) { + this.constantDenominator = constantDenominator; + } + /** * Normalizes the MeasureUnitImpl and generates the identifier string in place. */ @@ -244,7 +266,6 @@ public void serialize() { return; } - if (this.complexity == MeasureUnit.Complexity.COMPOUND) { // Note: don't sort a MIXED unit Collections.sort(this.getSingleUnits(), new SingleUnitComparator()); @@ -253,8 +274,7 @@ public void serialize() { StringBuilder result = new StringBuilder(); boolean beforePer = true; boolean firstTimeNegativeDimension = false; - for (SingleUnitImpl singleUnit : - this.getSingleUnits()) { + for (SingleUnitImpl singleUnit : this.getSingleUnits()) { if (beforePer && singleUnit.getDimensionality() < 0) { beforePer = false; firstTimeNegativeDimension = true; @@ -262,6 +282,12 @@ public void serialize() { firstTimeNegativeDimension = false; } + if (firstTimeNegativeDimension && this.constantDenominator > 0) { + result.append("-per-"); + result.append(this.constantDenominator); + firstTimeNegativeDimension = false; + } + if (this.getComplexity() == MeasureUnit.Complexity.MIXED) { if (result.length() != 0) { result.append("-and-"); @@ -283,6 +309,11 @@ public void serialize() { result.append(singleUnit.getNeutralIdentifier()); } + if (this.constantDenominator > 0) { + result.append("-per-"); + result.append(this.constantDenominator); + } + this.identifier = result.toString(); } @@ -405,6 +436,28 @@ public static class MeasureUnitImplWithIndex { } public static class UnitsParser { + + /** + * Contains a single unit or a constant. + * + * @throws IllegalArgumentException when both singleUnit and constant are + * existing. + * @param singleUnit the single unit + * @param constant the constant + */ + private class SingleUnitOrConstant { + SingleUnitImpl singleUnit; + Long constant; + + SingleUnitOrConstant(SingleUnitImpl singleUnit, Long constant) { + if (singleUnit != null && constant != null) { + throw new IllegalArgumentException("It is a SingleUnit Or a Constant, not both"); + } + this.singleUnit = singleUnit; + this.constant = constant; + } + } + // This used only to not build the trie each time we use the parser private volatile static CharsTrie savedTrie = null; @@ -417,14 +470,19 @@ public static class UnitsParser { // are in the denominator. Until we find an "-and-", at which point the // identifier is invalid pending TODO(CLDR-13701). private boolean fAfterPer = false; + + // Set to true when we just parsed a "-per-" or a "per-". + // This is used to ensure that the unit constant (such as "per-100-kilometer") + // can be parsed when it occurs after a "-per-" or a "per-". + private boolean fJustAfterPer = false; + // If an "-and-" was parsed prior to finding the "single - // * unit", sawAnd is set to true. If not, it is left as is. + // * unit", sawAnd is set to true. If not, it is left as is. private boolean fSawAnd = false; // Cache the MeasurePrefix values array to make getPrefixFromTrieIndex() // more efficient - private static MeasureUnit.MeasurePrefix[] measurePrefixValues = - MeasureUnit.MeasurePrefix.values(); + private static MeasureUnit.MeasurePrefix[] measurePrefixValues = MeasureUnit.MeasurePrefix.values(); private UnitsParser(String identifier) { this.fSource = identifier; @@ -514,7 +572,15 @@ private MeasureUnitImpl parse() { while (hasNext()) { fSawAnd = false; - SingleUnitImpl singleUnit = nextSingleUnit(); + SingleUnitOrConstant nextSingleUnitPair = nextSingleUnit(); + + if (nextSingleUnitPair.singleUnit == null) { + result.setConstantDenominator(nextSingleUnitPair.constant); + result.setComplexity(MeasureUnit.Complexity.COMPOUND); + continue; + } + + SingleUnitImpl singleUnit = nextSingleUnitPair.singleUnit; boolean added = result.appendSingleUnit(singleUnit); if (fSawAnd && !added) { @@ -526,10 +592,11 @@ private MeasureUnitImpl parse() { // same identifier. It doesn't fail for other compound units // (COMPOUND_PART_TIMES). Consequently we take care of that // here. - MeasureUnit.Complexity complexity = - fSawAnd ? MeasureUnit.Complexity.MIXED : MeasureUnit.Complexity.COMPOUND; + MeasureUnit.Complexity complexity = fSawAnd ? MeasureUnit.Complexity.MIXED + : MeasureUnit.Complexity.COMPOUND; if (result.getSingleUnits().size() == 2) { - // After appending two singleUnits, the complexity will be MeasureUnit.Complexity.COMPOUND + // After appending two singleUnits, the complexity will be + // MeasureUnit.Complexity.COMPOUND assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND; result.setComplexity(complexity); } else if (result.getComplexity() != complexity) { @@ -538,9 +605,25 @@ private MeasureUnitImpl parse() { } } + if (result.getSingleUnits().size() == 0) { + throw new IllegalArgumentException("Error in parsing a unit identifier."); + } + return result; } + /** + * Token states definitions. + */ + enum TokenState { + // No tokens seen yet (will accept power, SI or binary prefix, or simple unit) + NO_TOKENS_SEEN, + // Power token seen (will not accept another power token) + POWER_TOKEN_SEEN, + // SI or binary prefix token seen (will not accept a power, or SI or binary prefix token) + PREFIX_TOKEN_SEEN + } + /** * Returns the next "single unit" via result. *

@@ -548,27 +631,30 @@ private MeasureUnitImpl parse() { * dimensionality. *

* - * @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed - * compound units are not yet supported - TODO(CLDR-13701). + * @throws IllegalArgumentException if we parse both compound units and "-and-", + * since mixed + * compound units are not yet supported - + * TODO(CLDR-13701). */ - private SingleUnitImpl nextSingleUnit() { + private SingleUnitOrConstant nextSingleUnit() { SingleUnitImpl result = new SingleUnitImpl(); - // state: - // 0 = no tokens seen yet (will accept power, SI or binary prefix, or simple unit) - // 1 = power token seen (will not accept another power token) - // 2 = SI or binary prefix token seen (will not accept a power, or SI or binary prefix token) - int state = 0; + TokenState state = TokenState.NO_TOKENS_SEEN; boolean atStart = fIndex == 0; Token token = nextToken(); + fJustAfterPer = false; if (atStart) { + if (token.getType() == Token.Type.TYPE_UNIT_CONSTANT) { + throw new IllegalArgumentException("Unit constant cannot be the first token"); + } // Identifiers optionally start with "per-". if (token.getType() == Token.Type.TYPE_INITIAL_COMPOUND_PART) { assert token.getInitialCompoundPart() == InitialCompoundPart.INITIAL_COMPOUND_PART_PER; fAfterPer = true; + fJustAfterPer = true; result.setDimensionality(-1); token = nextToken(); @@ -589,6 +675,7 @@ private SingleUnitImpl nextSingleUnit() { } fAfterPer = true; + fJustAfterPer = true; result.setDimensionality(-1); break; @@ -610,30 +697,39 @@ private SingleUnitImpl nextSingleUnit() { token = nextToken(); } + // Treat unit constant + if (token.getType() == Token.Type.TYPE_UNIT_CONSTANT) { + if (!fJustAfterPer) { + throw new IllegalArgumentException("Unit constant cannot be the first token"); + } + + return new SingleUnitOrConstant(null, token.getConstantDenominator()); + } + // Read tokens until we have a complete SingleUnit or we reach the end. while (true) { switch (token.getType()) { case TYPE_POWER_PART: - if (state > 0) { + if (state != TokenState.NO_TOKENS_SEEN) { throw new IllegalArgumentException(); } result.setDimensionality(result.getDimensionality() * token.getPower()); - state = 1; + state = TokenState.POWER_TOKEN_SEEN; break; case TYPE_PREFIX: - if (state > 1) { + if (state == TokenState.PREFIX_TOKEN_SEEN) { throw new IllegalArgumentException(); } result.setPrefix(token.getPrefix()); - state = 2; + state = TokenState.PREFIX_TOKEN_SEEN; break; case TYPE_SIMPLE_UNIT: result.setSimpleUnit(token.getSimpleUnitIndex(), UnitsData.getSimpleUnits()); - return result; + return new SingleUnitOrConstant(result, null); default: throw new IllegalArgumentException(); @@ -653,95 +749,140 @@ private boolean hasNext() { private Token nextToken() { trie.reset(); - int match = -1; - // Saves the position in the fSource string for the end of the most - // recent matching token. - int previ = -1; + int matchingValue = -1; + // Saves the position in the `fSource` string at the end of the most + // recently matched token. + int prevIndex = -1; + + int savedIndex = fIndex; // Find the longest token that matches a value in the trie: while (fIndex < fSource.length()) { BytesTrie.Result result = trie.next(fSource.charAt(fIndex++)); if (result == BytesTrie.Result.NO_MATCH) { break; - } else if (result == BytesTrie.Result.NO_VALUE) { + } + + if (result == BytesTrie.Result.NO_VALUE) { continue; } - match = trie.getValue(); - previ = fIndex; + matchingValue = trie.getValue(); + prevIndex = fIndex; if (result == BytesTrie.Result.FINAL_VALUE) { break; } if (result != BytesTrie.Result.INTERMEDIATE_VALUE) { - throw new IllegalArgumentException("result must has an intermediate value"); + throw new IllegalArgumentException("Result must have an intermediate value"); } - - // continue; } + if (matchingValue < 0) { + if (fJustAfterPer) { + // We've just parsed a "per-", so we can expect a unit constant. + int hyphenIndex = fSource.indexOf('-', savedIndex); - if (match < 0) { - throw new IllegalArgumentException("Encountered unknown token starting at index " + previ); + // extract the unit constant from the string + String unitConstant = (hyphenIndex == -1) ? fSource.substring(savedIndex) + : fSource.substring(savedIndex, hyphenIndex); + fIndex = (hyphenIndex == -1) ? fSource.length() : hyphenIndex; + + return Token.tokenWithConstant(unitConstant); + + } else { + throw new IllegalArgumentException("Encountered unknown token starting at index " + prevIndex); + } } else { - fIndex = previ; + fIndex = prevIndex; } - return new Token(match); + return new Token(matchingValue); } static class Token { - private final int fMatch; + private final long fMatch; private final Type type; - public Token(int fMatch) { + public Token(long fMatch) { this.fMatch = fMatch; type = calculateType(fMatch); } + private Token(long fMatch, Type type) { + this.fMatch = fMatch; + this.type = type; + } + + public static Token tokenWithConstant(String constantStr) { + BigDecimal unitConstantValue = new BigDecimal(constantStr); + if (unitConstantValue.scale() <= 0 && unitConstantValue.compareTo(BigDecimal.ZERO) >= 0 + && unitConstantValue.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) <= 0) { + return new Token(unitConstantValue.longValueExact(), Type.TYPE_UNIT_CONSTANT); + } else { + throw new IllegalArgumentException( + "The unit constant value is not a valid non-negative long integer."); + } + } + public Type getType() { return this.type; } public MeasureUnit.MeasurePrefix getPrefix() { assert this.type == Type.TYPE_PREFIX; - return getPrefixFromTrieIndex(this.fMatch); + assert this.fMatch <= Integer.MAX_VALUE; + + int trieIndex = (int) this.fMatch; + return getPrefixFromTrieIndex(trieIndex); + } + + // Valid only for tokens with type TYPE_UNIT_CONSTANT. + public long getConstantDenominator() { + assert this.type == Type.TYPE_UNIT_CONSTANT; + return this.fMatch; } // Valid only for tokens with type TYPE_COMPOUND_PART. public int getMatch() { assert getType() == Type.TYPE_COMPOUND_PART; - return fMatch; + assert this.fMatch <= Integer.MAX_VALUE; + + int matchIndex = (int) this.fMatch; + return matchIndex; } // Even if there is only one InitialCompoundPart value, we have this // function for the simplicity of code consistency. public InitialCompoundPart getInitialCompoundPart() { - assert (this.type == Type.TYPE_INITIAL_COMPOUND_PART - && - fMatch == InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex()); - return InitialCompoundPart.getInitialCompoundPartFromTrieIndex(fMatch); + assert this.type == Type.TYPE_INITIAL_COMPOUND_PART; + assert fMatch == InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex(); + assert fMatch <= Integer.MAX_VALUE; + int trieIndex = (int) fMatch; + return InitialCompoundPart.getInitialCompoundPartFromTrieIndex(trieIndex); } public int getPower() { assert this.type == Type.TYPE_POWER_PART; - return PowerPart.getPowerFromTrieIndex(this.fMatch); + assert this.fMatch <= Integer.MAX_VALUE; + int trieIndex = (int) this.fMatch; + return PowerPart.getPowerFromTrieIndex(trieIndex); } public int getSimpleUnitIndex() { assert this.type == Type.TYPE_SIMPLE_UNIT; - return this.fMatch - UnitsData.Constants.kSimpleUnitOffset; + assert this.fMatch <= Integer.MAX_VALUE; + return ((int) this.fMatch) - UnitsData.Constants.kSimpleUnitOffset; } - // Calling calculateType() is invalid, resulting in an assertion failure, if Token - // value isn't positive. - private Type calculateType(int fMatch) { + // It is invalid to call calculateType() with a non-positive Token value, + // as it will result in an assertion failure. + private Type calculateType(long fMatch) { if (fMatch <= 0) { throw new AssertionError("fMatch must have a positive value"); } - if (fMatch < UnitsData.Constants.kCompoundPartOffset) { return Type.TYPE_PREFIX; } @@ -767,6 +908,7 @@ enum Type { TYPE_INITIAL_COMPOUND_PART, TYPE_POWER_PART, TYPE_SIMPLE_UNIT, + TYPE_UNIT_CONSTANT, } } } 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 1f94544f6f3b..066a2f6fb90f 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 @@ -44,14 +44,15 @@ public class MeasureUnit implements Serializable { private static final long serialVersionUID = -1839973855554750484L; // Cache of MeasureUnits. - // All access to the cache or cacheIsPopulated flag must be synchronized on class MeasureUnit, + // All access to the cache or cacheIsPopulated flag must be synchronized on + // class MeasureUnit, // i.e. from synchronized static methods. Beware of non-static methods. - private static final Map> cache - = new HashMap<>(); + private static final Map> cache = new HashMap<>(); private static boolean cacheIsPopulated = false; /** * If type set to null, measureUnitImpl is in use instead of type and subType. + * * @internal * @deprecated This API is ICU internal only. */ @@ -59,7 +60,9 @@ public class MeasureUnit implements Serializable { protected final String type; /** - * If subType set to null, measureUnitImpl is in use instead of type and subType. + * If subType set to null, measureUnitImpl is in use instead of type and + * subType. + * * @internal * @deprecated This API is ICU internal only. */ @@ -76,14 +79,18 @@ public class MeasureUnit implements Serializable { /** * Enumeration for unit complexity. There are three levels: *

- * The complexity determines which operations are available. For example, you cannot set the power + * The complexity determines which operations are available. For example, you + * cannot set the power * or prefix of a compound unit. * * @stable ICU 68 @@ -448,8 +455,6 @@ private MeasureUnit(MeasureUnitImpl measureUnitImpl) { this.measureUnitImpl = measureUnitImpl.copy(); } - - /** * Get the type, such as "length". May return null. * @@ -459,7 +464,6 @@ public String getType() { return type; } - /** * Get the subType, such as “foot”. May return null. * @@ -495,18 +499,21 @@ public Complexity getComplexity() { } /** - * Creates a MeasureUnit which is this SINGLE unit augmented with the specified prefix. + * Creates a MeasureUnit which is this SINGLE unit augmented with the specified + * prefix. * For example, MeasurePrefix.KILO for "kilo", or MeasurePrefix.KIBI for "kibi". * May return {@code this} if this unit already has that prefix. *

* There is sufficient locale data to format all standard prefixes. *

- * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an error will + * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an + * error will * occur. For more information, {@link Complexity}. * * @param prefix The prefix, from MeasurePrefix. * @return A new SINGLE unit. - * @throws UnsupportedOperationException if this unit is a COMPOUND or MIXED unit. + * @throws UnsupportedOperationException if this unit is a COMPOUND or MIXED + * unit. * @stable ICU 69 */ public MeasureUnit withPrefix(MeasurePrefix prefix) { @@ -531,10 +538,79 @@ public MeasurePrefix getPrefix() { } /** - * Returns the dimensionality (power) of this MeasureUnit. For example, if the unit is square, + * Creates a new MeasureUnit with a specified constant denominator. + *

+ * This method is applicable only to COMPOUND & SINGLE units. If invoked on a + * MIXED unit, an exception will be thrown. + * For further details, refer to {@link Complexity}. + *

+ * + * NOTE: If the constant denominator is set to 0, it means that you are removing + * the constant denominator. + * + * + * @param denominator The constant denominator to set. + * @return A new MeasureUnit with the specified constant denominator. + * @throws UnsupportedOperationException if the unit is not a COMPOUND unit. + * @draft ICU 77 + */ + public MeasureUnit withConstantDenominator(long denominator) { + if (this.getComplexity() == Complexity.MIXED) { + throw new UnsupportedOperationException( + "Constant denominator can only be applied to COMPOUND & SINGLE units"); + } + + MeasureUnitImpl measureUnitImpl = getCopyOfMeasureUnitImpl(); + measureUnitImpl.setConstantDenominator(denominator); + + measureUnitImpl.setComplexity(denominator == 0 && measureUnitImpl.getSingleUnits().size() == 1 + ? Complexity.SINGLE + : Complexity.COMPOUND); + + return measureUnitImpl.build(); + } + + /** + * Retrieves the constant denominator for this COMPOUND unit. + *

+ * Examples: + *

+ *

+ * This method is applicable only to COMPOUND units. If invoked on a SINGLE or + * MIXED unit, an exception will be thrown. + * For further details, refer to {@link Complexity}. + *

+ * + * NOTE: If no constant denominator exists, the method returns 0. + * + * @return The value of the constant denominator. + * @throws UnsupportedOperationException if the unit is not a COMPOUND unit. + * @draft ICU 77 + */ + public long getConstantDenominator() { + if (this.getComplexity() != Complexity.COMPOUND) { + throw new UnsupportedOperationException("Constant denominator is only supported for COMPOUND units"); + } + + if (this.measureUnitImpl == null) { + return 0; + } + + return this.measureUnitImpl.getConstantDenominator(); + } + + /** + * Returns the dimensionality (power) of this MeasureUnit. For example, if the + * unit is square, * then 2 is returned. *

- * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an exception will be thrown. + * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an + * exception will be thrown. * For more information, {@link Complexity}. * * @return The dimensionality (power) of this simple unit. @@ -546,10 +622,12 @@ public int getDimensionality() { } /** - * Creates a MeasureUnit which is this SINGLE unit augmented with the specified dimensionality + * Creates a MeasureUnit which is this SINGLE unit augmented with the specified + * dimensionality * (power). For example, if dimensionality is 2, the unit will be squared. *

- * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an exception is thrown. + * NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an + * exception is thrown. * For more information, {@link Complexity}. * * @param dimensionality The dimensionality (power). @@ -564,33 +642,47 @@ public MeasureUnit withDimensionality(int dimensionality) { } /** - * Computes the reciprocal of this MeasureUnit, with the numerator and denominator flipped. + * Computes the reciprocal of this MeasureUnit, with the numerator and + * denominator flipped. *

- * For example, if the receiver is "meter-per-second", the unit "second-per-meter" is returned. + * For example, if the receiver is "meter-per-second", the unit + * "second-per-meter" is returned. *

- * NOTE: Only works on SINGLE and COMPOUND units. If this is a MIXED unit, an error will + * NOTE: Only works on SINGLE and COMPOUND units. If this is a MIXED unit, an + * error will * occur. For more information, {@link Complexity}. * + *

+ * NOTE: An exception will be thrown for units that have a constant denominator. + * * @return The reciprocal of the target unit. - * @throws UnsupportedOperationException if the unit is MIXED. + * @throws UnsupportedOperationException if the unit is MIXED or has a constant + * denominator. * @stable ICU 68 */ public MeasureUnit reciprocal() { + if (this.getComplexity() == Complexity.COMPOUND && this.getConstantDenominator() != 0) { + throw new UnsupportedOperationException("Cannot take reciprocal of a unit with a constant denominator"); + } + MeasureUnitImpl measureUnit = getCopyOfMeasureUnitImpl(); measureUnit.takeReciprocal(); return measureUnit.build(); } /** - * Computes the product of this unit with another unit. This is a way to build units from + * Computes the product of this unit with another unit. This is a way to build + * units from * constituent parts. *

* The numerator and denominator are preserved through this operation. *

- * For example, if the receiver is "kilowatt" and the argument is "hour-per-day", then the + * For example, if the receiver is "kilowatt" and the argument is + * "hour-per-day", then the * unit "kilowatt-hour-per-day" is returned. *

- * NOTE: Only works on SINGLE and COMPOUND units. If either unit (receivee and argument) is a + * NOTE: Only works on SINGLE and COMPOUND units. If either unit (receivee and + * argument) is a * MIXED unit, an error will occur. For more information, {@link Complexity}. * * @param other The MeasureUnit to multiply with the target. @@ -610,8 +702,7 @@ public MeasureUnit product(MeasureUnit other) { throw new UnsupportedOperationException(); } - for (SingleUnitImpl singleUnit : - otherImplRef.getSingleUnits()) { + for (SingleUnitImpl singleUnit : otherImplRef.getSingleUnits()) { implCopy.appendSingleUnit(singleUnit); } @@ -619,7 +710,8 @@ public MeasureUnit product(MeasureUnit other) { } /** - * Returns the list of SINGLE units contained within a sequence of COMPOUND units. + * Returns the list of SINGLE units contained within a sequence of COMPOUND + * units. *

* Examples: * - Given "meter-kilogram-per-second", three units will be returned: "meter", @@ -628,13 +720,18 @@ public MeasureUnit product(MeasureUnit other) { * and "second". *

* If this is a SINGLE unit, a list of length 1 will be returned. - * + * + *

+ * NOTE: For units with a constant denominator, the returned single units will + * not include the constant denominator. + * To obtain the constant denominator, retrieve it from the original unit. + *

+ * * @return An unmodifiable list of single units * @stable ICU 68 */ public List splitToSingleUnits() { - final ArrayList singleUnits = - getMaybeReferenceOfMeasureUnitImpl().getSingleUnits(); + final ArrayList singleUnits = getMaybeReferenceOfMeasureUnitImpl().getSingleUnits(); List result = new ArrayList<>(singleUnits.size()); for (SingleUnitImpl singleUnit : singleUnits) { result.add(singleUnit.build()); @@ -693,6 +790,7 @@ public static Set getAvailableTypes() { /** * For the given type, return the available units. + * * @param type the type * @return the available units for type. Returned set is unmodifiable. * @stable ICU 53 @@ -725,9 +823,11 @@ public synchronized static Set getAvailable() { } /** - * Creates a MeasureUnit instance (creates a singleton instance) or returns one from the cache. + * Creates a MeasureUnit instance (creates a singleton instance) or returns one + * from the cache. *

- * Normally this method should not be used, since there will be no formatting data + * Normally this method should not be used, since there will be no formatting + * data * available for it, and it may not be returned by getAvailable(). * However, for special purposes (such as CLDR tooling), it is available. * @@ -804,7 +904,7 @@ public MeasureUnit create(String unusedType, String subType) { static Factory TIMEUNIT_FACTORY = new Factory() { @Override public MeasureUnit create(String type, String subType) { - return new TimeUnit(type, subType); + return new TimeUnit(type, subType); } }; @@ -816,7 +916,8 @@ private static final class MeasureUnitSink extends UResource.Sink { public void put(UResource.Key key, UResource.Value value, boolean noFallback) { UResource.Table unitTypesTable = value.getTable(); for (int i2 = 0; unitTypesTable.getKeyAndValue(i2, key, value); ++i2) { - // Skip "compound" and "coordinate" since they are treated differently from the other units + // Skip "compound" and "coordinate" since they are treated differently from the + // other units if (key.contentEquals("compound") || key.contentEquals("coordinate")) { continue; } @@ -849,7 +950,8 @@ public void put(UResource.Key key, UResource.Value value, boolean noFallback) { * Population is done lazily, in response to MeasureUnit.getAvailable() * or other API that expects to see all of the MeasureUnits. * - *

At static initialization time the MeasureUnits cache is populated + *

+ * At static initialization time the MeasureUnits cache is populated * with public static instances (G_FORCE, METER_PER_SECOND_SQUARED, etc.) only. * Adding of others is deferred until later to avoid circular static init * dependencies with classes Currency and TimeUnit.