ICU-22781 Adding support for constant denominators

See #3336
This commit is contained in:
Younies Mahmoud 2025-01-21 14:04:07 +00:00
parent 384c54ce66
commit 373cbaf3b2
3 changed files with 428 additions and 89 deletions

View file

@ -18,6 +18,7 @@ import java.lang.reflect.Field;
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.NoUnit;
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 class MeasureUnitTest extends CoreTestFmwk {
}
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,83 @@ public class MeasureUnitTest extends CoreTestFmwk {
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<ConstantDenominatorTestCase> 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),
// Test for constant denominators that are powers of 10
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),
// Test for constant denominators that are represented as scientific notation
// numbers.
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),
// Test for constant denominators that are randomely selected.
new ConstantDenominatorTestCase("liter-per-12345-kilometer", 12345),
new ConstantDenominatorTestCase("per-1000-kilometer", 1000),
new ConstantDenominatorTestCase("liter-per-1000-kiloliter", 1000),
// Test for constant denominators that gives 0.
new ConstantDenominatorTestCase("meter", 0),
new ConstantDenominatorTestCase("meter-per-second", 0),
new ConstantDenominatorTestCase("meter-per-square-second", 0),
// 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());
assertTrue("Complexity for " + testCase.identifier,
unit.getComplexity() == Complexity.COMPOUND || unit.getComplexity() == Complexity.SINGLE);
}
}
@Test
public void TestInvalidIdentifiers() {
final String inputs[] = {
@ -1348,6 +1429,19 @@ public class MeasureUnitTest extends CoreTestFmwk {
// Compound units not supported in mixed units yet. TODO(CLDR-13701).
"kilonewton-meter-and-newton-meter",
// Invalid units because of invalid constant denominator
"meter-per--20--second",
"meter-per-1000-1e9-second",
"meter-per-1e20-second",
"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) {

View file

@ -14,6 +14,7 @@ import com.ibm.icu.util.ICUCloneNotSupportedException;
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 class MeasureUnitImpl {
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 class MeasureUnitImpl {
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 class MeasureUnitImpl {
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 class MeasureUnitImpl {
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 class MeasureUnitImpl {
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 class MeasureUnitImpl {
result.append(singleUnit.getNeutralIdentifier());
}
if (this.constantDenominator > 0) {
result.append("-per-");
result.append(this.constantDenominator);
}
this.identifier = result.toString();
}
@ -405,6 +436,28 @@ public class MeasureUnitImpl {
}
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 class MeasureUnitImpl {
// 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 @@ public class MeasureUnitImpl {
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 @@ public class MeasureUnitImpl {
// 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 @@ public class MeasureUnitImpl {
}
}
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.
* <p>
@ -548,27 +631,30 @@ public class MeasureUnitImpl {
* dimensionality.
* <p>
*
* @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 @@ public class MeasureUnitImpl {
}
fAfterPer = true;
fJustAfterPer = true;
result.setDimensionality(-1);
break;
@ -610,30 +697,39 @@ public class MeasureUnitImpl {
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 @@ public class MeasureUnitImpl {
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 @@ public class MeasureUnitImpl {
TYPE_INITIAL_COMPOUND_PART,
TYPE_POWER_PART,
TYPE_SIMPLE_UNIT,
TYPE_UNIT_CONSTANT,
}
}
}

View file

@ -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<String, Map<String,MeasureUnit>> cache
= new HashMap<>();
private static final Map<String, Map<String, MeasureUnit>> 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:
* <ul>
* <li>SINGLE: A single unit, optionally with a power and/or SI or binary prefix.
* <li>SINGLE: A single unit, optionally with a power and/or SI or binary
* prefix.
* Examples: hectare, square-kilometer, kilojoule, per-second, mebibyte.</li>
* <li>COMPOUND: A unit composed of the product of multiple single units. Examples:
* <li>COMPOUND: A unit composed of the product of multiple single units.
* Examples:
* meter-per-second, kilowatt-hour, kilogram-meter-per-square-second.</li>
* <li>MIXED: A unit composed of the sum of multiple single units. Examples: foot-and-inch,
* <li>MIXED: A unit composed of the sum of multiple single units. Examples:
* foot-and-inch,
* hour-and-minute-and-second, degree-and-arcminute-and-arcsecond.</li>
* </ul>
* 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 @@ public class MeasureUnit implements Serializable {
this.measureUnitImpl = measureUnitImpl.copy();
}
/**
* Get the type, such as "length". May return null.
*
@ -459,7 +464,6 @@ public class MeasureUnit implements Serializable {
return type;
}
/**
* Get the subType, such as foot. May return null.
*
@ -495,18 +499,21 @@ public class MeasureUnit implements Serializable {
}
/**
* 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.
* <p>
* There is sufficient locale data to format all standard prefixes.
* <p>
* 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,80 @@ public class MeasureUnit implements Serializable {
}
/**
* Returns the dimensionality (power) of this MeasureUnit. For example, if the unit is square,
* Creates a new MeasureUnit with a specified constant denominator.
* <p>
* 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}.
* <p>
*
* 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.COMPOUND && this.getComplexity() != Complexity.SINGLE) {
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.
* <p>
* Examples:
* <ul>
* <li>For the unit "liter-per-1000-kiloliter", the constant denominator is
* 1000.</li>
* <li>For the unit "liter-per-kilometer", the constant denominator is
* zero.</li>
* </ul>
* <p>
* 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}.
* <p>
*
* 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 && this.getComplexity() != Complexity.SINGLE) {
throw new UnsupportedOperationException(
"Constant denominator is only supported for COMPOUND & SINGLE 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.
* <p>
* 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 +623,12 @@ public class MeasureUnit implements Serializable {
}
/**
* 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.
* <p>
* 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 +643,47 @@ public class MeasureUnit implements Serializable {
}
/**
* Computes the reciprocal of this MeasureUnit, with the numerator and denominator flipped.
* Computes the reciprocal of this MeasureUnit, with the numerator and
* denominator flipped.
* <p>
* 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.
* <p>
* 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}.
*
* <p>
* 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.
* <p>
* The numerator and denominator are preserved through this operation.
* <p>
* 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.
* <p>
* 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 +703,7 @@ public class MeasureUnit implements Serializable {
throw new UnsupportedOperationException();
}
for (SingleUnitImpl singleUnit :
otherImplRef.getSingleUnits()) {
for (SingleUnitImpl singleUnit : otherImplRef.getSingleUnits()) {
implCopy.appendSingleUnit(singleUnit);
}
@ -619,7 +711,8 @@ public class MeasureUnit implements Serializable {
}
/**
* 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.
* <p>
* Examples:
* - Given "meter-kilogram-per-second", three units will be returned: "meter",
@ -628,13 +721,18 @@ public class MeasureUnit implements Serializable {
* and "second".
* <p>
* If this is a SINGLE unit, a list of length 1 will be returned.
*
*
* <p>
* 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.
* <p>
*
* @return An unmodifiable list of single units
* @stable ICU 68
*/
public List<MeasureUnit> splitToSingleUnits() {
final ArrayList<SingleUnitImpl> singleUnits =
getMaybeReferenceOfMeasureUnitImpl().getSingleUnits();
final ArrayList<SingleUnitImpl> singleUnits = getMaybeReferenceOfMeasureUnitImpl().getSingleUnits();
List<MeasureUnit> result = new ArrayList<>(singleUnits.size());
for (SingleUnitImpl singleUnit : singleUnits) {
result.add(singleUnit.build());
@ -693,6 +791,7 @@ public class MeasureUnit implements Serializable {
/**
* 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 +824,11 @@ public class MeasureUnit implements Serializable {
}
/**
* 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.
* <p>
* 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 +905,7 @@ public class MeasureUnit implements Serializable {
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 +917,8 @@ public class MeasureUnit implements Serializable {
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 +951,8 @@ public class MeasureUnit implements Serializable {
* Population is done lazily, in response to MeasureUnit.getAvailable()
* or other API that expects to see all of the MeasureUnits.
*
* <p>At static initialization time the MeasureUnits cache is populated
* <p>
* 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.