ICU-21010 MeasureUnit extension in Java

See #1275
This commit is contained in:
younies 2020-09-14 16:28:03 +00:00 committed by Younies Mahmoud
parent a8a699b88e
commit d149031433
5 changed files with 1911 additions and 38 deletions

View file

@ -0,0 +1,737 @@
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import com.ibm.icu.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class MeasureUnitImpl {
/**
* The full unit identifier. Null if not computed.
*/
private String identifier = null;
/**
* The complexity, either SINGLE, COMPOUND, or MIXED.
*/
private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE;
/**
* The list of simple units. These may be summed or multiplied, based on the
* value of the complexity field.
* <p>
* The "dimensionless" unit (SingleUnitImpl default constructor) must not be
* added to this list.
* <p>
* The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>.
*/
private ArrayList<SingleUnitImpl> singleUnits;
public MeasureUnitImpl() {
singleUnits = new ArrayList<>();
}
public MeasureUnitImpl(SingleUnitImpl singleUnit) {
this();
this.appendSingleUnit(singleUnit);
}
/**
* Parse a unit identifier into a MeasureUnitImpl.
*
* @param identifier The unit identifier string.
* @return A newly parsed object.
* @throws <code>IllegalArgumentException</code> in case of incorrect/non-parsed identifier.
*/
public static MeasureUnitImpl forIdentifier(String identifier) {
return UnitsParser.parseForIdentifier(identifier);
}
/**
* Used for currency units.
*/
public static MeasureUnitImpl forCurrencyCode(String currencyCode) {
MeasureUnitImpl result = new MeasureUnitImpl();
result.identifier = currencyCode;
return result;
}
public MeasureUnitImpl clone() {
MeasureUnitImpl result = new MeasureUnitImpl();
result.complexity = this.complexity;
result.identifier = this.identifier;
result.singleUnits = (ArrayList<SingleUnitImpl>) this.singleUnits.clone();
return result;
}
/**
* Returns the list of simple units.
*/
public ArrayList<SingleUnitImpl> getSingleUnits() {
return singleUnits;
}
/**
* Mutates this MeasureUnitImpl to take the reciprocal.
*/
public void takeReciprocal() {
this.identifier = null;
for (SingleUnitImpl singleUnit :
this.singleUnits) {
singleUnit.setDimensionality(singleUnit.getDimensionality() * -1);
}
}
/**
* Extracts the list of all the individual units inside the `MeasureUnitImpl`.
* For example:
* - if the <code>MeasureUnitImpl</code> is <code>foot-per-hour</code>
* it will return a list of 1 <code>{foot-per-hour}</code>
* - if the <code>MeasureUnitImpl</code> is <code>foot-and-inch</code>
* it will return a list of 2 <code>{ foot, inch}</code>
*
* @return a list of <code>MeasureUnitImpl</code>
*/
public ArrayList<MeasureUnitImpl> extractIndividualUnits() {
ArrayList<MeasureUnitImpl> result = new ArrayList<MeasureUnitImpl>();
if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
// In case of mixed units, each single unit can be considered as a stand alone MeasureUnitImpl.
for (SingleUnitImpl singleUnit :
this.getSingleUnits()) {
result.add(new MeasureUnitImpl(singleUnit));
}
return result;
}
result.add(this.clone());
return result;
}
/**
* Mutates this MeasureUnitImpl to append a single unit.
*
* @return true if a new item was added. If unit is the dimensionless unit,
* it is never added: the return value will always be false.
*/
public boolean appendSingleUnit(SingleUnitImpl singleUnit) {
identifier = null;
if (singleUnit == null) {
// We don't append dimensionless units.
return false;
}
// Find a similar unit that already exists, to attempt to coalesce
SingleUnitImpl oldUnit = null;
for (int i = 0, n = this.singleUnits.size(); i < n; i++) {
SingleUnitImpl candidate = this.singleUnits.get(i);
if (candidate.isCompatibleWith(singleUnit)) {
oldUnit = candidate;
break;
}
}
if (oldUnit != null) {
// Both dimensionalities will be positive, or both will be negative, by
// virtue of isCompatibleWith().
oldUnit.setDimensionality(oldUnit.getDimensionality() + singleUnit.getDimensionality());
return false;
}
// Add a copy of singleUnit
this.singleUnits.add(singleUnit.clone());
// If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the singleUnits are more
// than one singleUnit. thus means the complexity should be `UMEASURE_UNIT_COMPOUND`
if (this.singleUnits.size() > 1 && this.complexity == MeasureUnit.Complexity.SINGLE) {
this.setComplexity(MeasureUnit.Complexity.COMPOUND);
}
return true;
}
/**
* Transform this MeasureUnitImpl into a MeasureUnit, simplifying if possible.
* <p>
* NOTE: this function must be called from a thread-safe class
*/
public MeasureUnit build() {
return MeasureUnit.fromMeasureUnitImpl(this);
}
/**
* @return SingleUnitImpl
* @throws UnsupportedOperationException if the object could not be converted to SingleUnitImpl.
*/
public SingleUnitImpl getSingleUnitImpl() {
if (this.singleUnits.size() == 0) {
return new SingleUnitImpl();
}
if (this.singleUnits.size() == 1) {
return this.singleUnits.get(0).clone();
}
throw new UnsupportedOperationException();
}
/**
* Returns the CLDR unit identifier and null if not computed.
*/
public String getIdentifier() {
return identifier;
}
public MeasureUnit.Complexity getComplexity() {
return complexity;
}
public void setComplexity(MeasureUnit.Complexity complexity) {
this.complexity = complexity;
}
/**
* Normalizes the MeasureUnitImpl and generates the identifier string in place.
*/
public void serialize() {
if (this.getSingleUnits().size() == 0) {
// Dimensionless, constructed by the default constructor: no appending
// to this.result, we wish it to contain the zero-length string.
return;
}
if (this.complexity == MeasureUnit.Complexity.COMPOUND) {
// Note: don't sort a MIXED unit
Collections.sort(this.getSingleUnits(), new SingleUnitComparator());
}
StringBuilder result = new StringBuilder();
boolean beforePer = true;
boolean firstTimeNegativeDimension = false;
for (SingleUnitImpl singleUnit :
this.getSingleUnits()) {
if (beforePer && singleUnit.getDimensionality() < 0) {
beforePer = false;
firstTimeNegativeDimension = true;
} else if (singleUnit.getDimensionality() < 0) {
firstTimeNegativeDimension = false;
}
String singleUnitIdentifier = singleUnit.getNeutralIdentifier();
if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
if (result.length() != 0) {
result.append("-and-");
}
} else {
if (firstTimeNegativeDimension) {
if (result.length() == 0) {
result.append("per-");
} else {
result.append("-per-");
}
} else {
if (result.length() != 0) {
result.append("-");
}
}
}
result.append(singleUnitIdentifier);
}
this.identifier = result.toString();
}
public enum CompoundPart {
// Represents "-per-"
PER(0),
// Represents "-"
TIMES(1),
// Represents "-and-"
AND(2);
private final int index;
CompoundPart(int index) {
this.index = index;
}
public static CompoundPart getCompoundPartFromTrieIndex(int trieIndex) {
int index = trieIndex - UnitsData.Constants.kCompoundPartOffset;
switch (index) {
case 0:
return CompoundPart.PER;
case 1:
return CompoundPart.TIMES;
case 2:
return CompoundPart.AND;
default:
throw new AssertionError("CompoundPart index must be 0, 1 or 2");
}
}
public int getTrieIndex() {
return this.index + UnitsData.Constants.kCompoundPartOffset;
}
public int getValue() {
return index;
}
}
public enum PowerPart {
P2(2),
P3(3),
P4(4),
P5(5),
P6(6),
P7(7),
P8(8),
P9(9),
P10(10),
P11(11),
P12(12),
P13(13),
P14(14),
P15(15);
private final int power;
PowerPart(int power) {
this.power = power;
}
public static int getPowerFromTrieIndex(int trieIndex) {
return trieIndex - UnitsData.Constants.kPowerPartOffset;
}
public int getTrieIndex() {
return this.power + UnitsData.Constants.kPowerPartOffset;
}
public int getValue() {
return power;
}
}
public enum InitialCompoundPart {
// Represents "per-", the only compound part that can appear at the start of
// an identifier.
INITIAL_COMPOUND_PART_PER(0);
private final int index;
InitialCompoundPart(int powerIndex) {
this.index = powerIndex;
}
public static InitialCompoundPart getInitialCompoundPartFromTrieIndex(int trieIndex) {
int index = trieIndex - UnitsData.Constants.kInitialCompoundPartOffset;
if (index == 0) {
return INITIAL_COMPOUND_PART_PER;
}
throw new IllegalArgumentException("Incorrect trieIndex");
}
public int getTrieIndex() {
return this.index + UnitsData.Constants.kInitialCompoundPartOffset;
}
public int getValue() {
return index;
}
}
public static class UnitsParser {
// This used only to not build the trie each time we use the parser
private volatile static CharsTrie savedTrie = null;
private final String[] simpleUnits;
// This trie used in the parsing operation.
private CharsTrie trie;
// Tracks parser progress: the offset into fSource.
private int fIndex = 0;
// Set to true when we've seen a "-per-" or a "per-", after which all units
// are in the denominator. Until we find an "-and-", at which point the
// identifier is invalid pending TODO(CLDR-13700).
private boolean fAfterPer = false;
private String fSource;
// If an "-and-" was parsed prior to finding the "single
// * unit", sawAnd is set to true. If not, it is left as is.
private boolean fSawAnd = false;
private UnitsParser(String identifier) {
this.simpleUnits = UnitsData.getSimpleUnits();
this.fSource = identifier;
if (UnitsParser.savedTrie != null) {
try {
this.trie = UnitsParser.savedTrie.clone();
} catch (CloneNotSupportedException e) {
throw new ICUCloneNotSupportedException();
}
return;
}
// Building the trie.
CharsTrieBuilder trieBuilder;
trieBuilder = new CharsTrieBuilder();
// Add syntax parts (compound, power prefixes)
trieBuilder.add("-per-", CompoundPart.PER.getTrieIndex());
trieBuilder.add("-", CompoundPart.TIMES.getTrieIndex());
trieBuilder.add("-and-", CompoundPart.AND.getTrieIndex());
trieBuilder.add("per-", InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex());
trieBuilder.add("square-", PowerPart.P2.getTrieIndex());
trieBuilder.add("cubic-", PowerPart.P3.getTrieIndex());
trieBuilder.add("pow2-", PowerPart.P2.getTrieIndex());
trieBuilder.add("pow3-", PowerPart.P3.getTrieIndex());
trieBuilder.add("pow4-", PowerPart.P4.getTrieIndex());
trieBuilder.add("pow5-", PowerPart.P5.getTrieIndex());
trieBuilder.add("pow6-", PowerPart.P6.getTrieIndex());
trieBuilder.add("pow7-", PowerPart.P7.getTrieIndex());
trieBuilder.add("pow8-", PowerPart.P8.getTrieIndex());
trieBuilder.add("pow9-", PowerPart.P9.getTrieIndex());
trieBuilder.add("pow10-", PowerPart.P10.getTrieIndex());
trieBuilder.add("pow11-", PowerPart.P11.getTrieIndex());
trieBuilder.add("pow12-", PowerPart.P12.getTrieIndex());
trieBuilder.add("pow13-", PowerPart.P13.getTrieIndex());
trieBuilder.add("pow14-", PowerPart.P14.getTrieIndex());
trieBuilder.add("pow15-", PowerPart.P15.getTrieIndex());
// Add SI prefixes
for (MeasureUnit.SIPrefix siPrefix :
MeasureUnit.SIPrefix.values()) {
trieBuilder.add(siPrefix.getIdentifier(), getTrieIndex(siPrefix));
}
// Add simple units
for (int i = 0; i < simpleUnits.length; i++) {
trieBuilder.add(simpleUnits[i], i + UnitsData.Constants.kSimpleUnitOffset);
}
// TODO: Use SLOW or FAST here?
UnitsParser.savedTrie = trieBuilder.build(StringTrieBuilder.Option.FAST);
try {
this.trie = UnitsParser.savedTrie.clone();
} catch (CloneNotSupportedException e) {
throw new ICUCloneNotSupportedException();
}
}
/**
* Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35.
* Validates and canonicalizes the identifier.
*
* @return MeasureUnitImpl object or null if the identifier is empty.
* @throws IllegalArgumentException in case of invalid identifier.
*/
public static MeasureUnitImpl parseForIdentifier(String identifier) {
if (identifier == null || identifier.isEmpty()) {
return null;
}
UnitsParser parser = new UnitsParser(identifier);
return parser.parse();
}
private static MeasureUnit.SIPrefix getSiPrefixFromTrieIndex(int trieIndex) {
for (MeasureUnit.SIPrefix element :
MeasureUnit.SIPrefix.values()) {
if (getTrieIndex(element) == trieIndex)
return element;
}
throw new IllegalArgumentException("Incorrect trieIndex");
}
private static int getTrieIndex(MeasureUnit.SIPrefix prefix) {
return prefix.getSiPrefixPower() + UnitsData.Constants.kSIPrefixOffset;
}
private MeasureUnitImpl parse() {
MeasureUnitImpl result = new MeasureUnitImpl();
if (fSource.isEmpty()) {
// The dimensionless unit: nothing to parse. return null.
return null;
}
while (hasNext()) {
fSawAnd = false;
SingleUnitImpl singleUnit = nextSingleUnit();
boolean added = result.appendSingleUnit(singleUnit);
if (fSawAnd && !added) {
throw new IllegalArgumentException("Two similar units are not allowed in a mixed unit.");
}
if ((result.singleUnits.size()) >= 2) {
// nextSingleUnit fails appropriately for "per" and "and" in the
// 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;
if (result.getSingleUnits().size() == 2) {
// After appending two singleUnits, the complexity will be `UMEASURE_UNIT_COMPOUND`
assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND;
result.setComplexity(complexity);
} else if (result.getComplexity() != complexity) {
throw new IllegalArgumentException("Can't have mixed compound units");
}
}
}
return result;
}
/**
* Returns the next "single unit" via result.
* <p>
* If a "-per-" was parsed, the result will have appropriate negative
* dimensionality.
* <p>
*
* @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed
* compound units are not yet supported - TODO(CLDR-13700).
*/
private SingleUnitImpl nextSingleUnit() {
SingleUnitImpl result = new SingleUnitImpl();
// state:
// 0 = no tokens seen yet (will accept power, SI prefix, or simple unit)
// 1 = power token seen (will not accept another power token)
// 2 = SI prefix token seen (will not accept a power or SI prefix token)
int state = 0;
boolean atStart = fIndex == 0;
Token token = nextToken();
if (atStart) {
// Identifiers optionally start with "per-".
if (token.getType() == Token.Type.TYPE_INITIAL_COMPOUND_PART) {
assert token.getInitialCompoundPart() == InitialCompoundPart.INITIAL_COMPOUND_PART_PER;
fAfterPer = true;
result.setDimensionality(-1);
token = nextToken();
}
} else {
// All other SingleUnit's are separated from previous SingleUnit's
// via a compound part:
if (token.getType() != Token.Type.TYPE_COMPOUND_PART) {
throw new IllegalArgumentException("token type must be TYPE_COMPOUND_PART");
}
CompoundPart compoundPart = CompoundPart.getCompoundPartFromTrieIndex(token.getMatch());
switch (compoundPart) {
case PER:
if (fSawAnd) {
throw new IllegalArgumentException("Mixed compound units not yet supported");
// TODO(CLDR-13700).
}
fAfterPer = true;
result.setDimensionality(-1);
break;
case TIMES:
if (fAfterPer) {
result.setDimensionality(-1);
}
break;
case AND:
if (fAfterPer) {
// not yet supported, TODO(CLDR-13700).
throw new IllegalArgumentException("Can't start with \"-and-\", and mixed compound units");
}
fSawAnd = true;
break;
}
token = nextToken();
}
// 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) {
throw new IllegalArgumentException();
}
result.setDimensionality(result.getDimensionality() * token.getPower());
state = 1;
break;
case TYPE_SI_PREFIX:
if (state > 1) {
throw new IllegalArgumentException();
}
result.setSiPrefix(token.getSIPrefix());
state = 2;
break;
case TYPE_SIMPLE_UNIT:
result.setSimpleUnit(token.getSimpleUnitIndex(), simpleUnits);
return result;
default:
throw new IllegalArgumentException();
}
if (!hasNext()) {
throw new IllegalArgumentException("We ran out of tokens before finding a complete single unit.");
}
token = nextToken();
}
}
private boolean hasNext() {
return fIndex < fSource.length();
}
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;
// 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) {
continue;
}
match = trie.getValue();
previ = fIndex;
if (result == BytesTrie.Result.FINAL_VALUE) {
break;
}
if (result != BytesTrie.Result.INTERMEDIATE_VALUE) {
throw new IllegalArgumentException("result must has an intermediate value");
}
// continue;
}
if (match < 0) {
throw new IllegalArgumentException("Encountered unknown token starting at index " + previ);
} else {
fIndex = previ;
}
return new Token(match);
}
static class Token {
private final int fMatch;
private final Type type;
public Token(int fMatch) {
this.fMatch = fMatch;
type = calculateType(fMatch);
}
public Type getType() {
return this.type;
}
public MeasureUnit.SIPrefix getSIPrefix() {
assert this.type == Type.TYPE_SI_PREFIX;
return getSiPrefixFromTrieIndex(this.fMatch);
}
// Valid only for tokens with type TYPE_COMPOUND_PART.
public int getMatch() {
assert getType() == Type.TYPE_COMPOUND_PART;
return fMatch;
}
// 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);
}
public int getPower() {
assert this.type == Type.TYPE_POWER_PART;
return PowerPart.getPowerFromTrieIndex(this.fMatch);
}
public int getSimpleUnitIndex() {
return 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) {
if (fMatch <= 0) {
throw new AssertionError("fMatch must have a positive value");
}
if (fMatch < UnitsData.Constants.kCompoundPartOffset) {
return Type.TYPE_SI_PREFIX;
}
if (fMatch < UnitsData.Constants.kInitialCompoundPartOffset) {
return Type.TYPE_COMPOUND_PART;
}
if (fMatch < UnitsData.Constants.kPowerPartOffset) {
return Type.TYPE_INITIAL_COMPOUND_PART;
}
if (fMatch < UnitsData.Constants.kSimpleUnitOffset) {
return Type.TYPE_POWER_PART;
}
return Type.TYPE_SIMPLE_UNIT;
}
enum Type {
TYPE_UNDEFINED,
TYPE_SI_PREFIX,
// Token type for "-per-", "-", and "-and-".
TYPE_COMPOUND_PART,
// Token type for "per-".
TYPE_INITIAL_COMPOUND_PART,
TYPE_POWER_PART,
TYPE_SIMPLE_UNIT,
}
}
}
class SingleUnitComparator implements Comparator<SingleUnitImpl> {
@Override
public int compare(SingleUnitImpl o1, SingleUnitImpl o2) {
return o1.compareTo(o2);
}
}
}

View file

@ -0,0 +1,157 @@
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import com.ibm.icu.util.MeasureUnit;
public class SingleUnitImpl {
/**
* Simple unit index, unique for every simple unit, -1 for the dimensionless
* unit. This is an index into a string list in unit.txt {ConversionUnits}.
* <p>
* The default value is -1, meaning the dimensionless unit:
* isDimensionless() will return true, until index is changed.
*/
private int index = -1;
/**
* SimpleUnit is the simplest form of a Unit. For example, for "square-millimeter", the simple unit would be "meter"Ò
* <p>
* The default value is "", meaning the dimensionless unit:
* isDimensionless() will return true, until index is changed.
*/
private String simpleUnit = "";
/**
* Determine the power of the `SingleUnit`. For example, for "square-meter", the dimensionality will be `2`.
* <p>
* NOTE:
* Default dimensionality is 1.
*/
private int dimensionality = 1;
/**
* SI Prefix
*/
private MeasureUnit.SIPrefix siPrefix = MeasureUnit.SIPrefix.ONE;
public SingleUnitImpl clone() {
SingleUnitImpl result = new SingleUnitImpl();
result.index = this.index;
result.dimensionality = this.dimensionality;
result.simpleUnit = this.simpleUnit;
result.siPrefix = this.siPrefix;
return result;
}
public MeasureUnit build() {
MeasureUnitImpl measureUnit = new MeasureUnitImpl(this);
return measureUnit.build();
}
/**
* Generates an neutral identifier string for a single unit which means we do not include the dimension signal.
*
* @throws IllegalArgumentException
*/
public String getNeutralIdentifier() {
StringBuilder result = new StringBuilder();
int posPower = Math.abs(this.getDimensionality());
assert posPower > 0 : "getIdentifier does not support the dimensionless";
if (posPower == 1) {
// no-op
} else if (posPower == 2) {
result.append("square-");
} else if (posPower == 3) {
result.append("cubic-");
} else if (posPower <= 15) {
result.append("pow");
result.append(posPower);
result.append('-');
} else {
throw new IllegalArgumentException("Unit Identifier Syntax Error");
}
result.append(this.getSiPrefix().getIdentifier());
result.append(this.getSimpleUnit());
return result.toString();
}
/**
* Compare this SingleUnitImpl to another SingleUnitImpl for the sake of
* sorting and coalescing.
* <p>
* Takes the sign of dimensionality into account, but not the absolute
* value: per-meter is not considered the same as meter, but meter is
* considered the same as square-meter.
* <p>
* The dimensionless unit generally does not get compared, but if it did, it
* would sort before other units by virtue of index being < 0 and
* dimensionality not being negative.
*/
int compareTo(SingleUnitImpl other) {
if (dimensionality < 0 && other.dimensionality > 0) {
// Positive dimensions first
return 1;
}
if (dimensionality > 0 && other.dimensionality < 0) {
return -1;
}
if (index < other.index) {
return -1;
}
if (index > other.index) {
return 1;
}
if (this.getSiPrefix().getSiPrefixPower() < other.getSiPrefix().getSiPrefixPower()) {
return -1;
}
if (this.getSiPrefix().getSiPrefixPower() > other.getSiPrefix().getSiPrefixPower()) {
return 1;
}
return 0;
}
/**
* Checks whether this SingleUnitImpl is compatible with another for the purpose of coalescing.
* <p>
* Units with the same base unit and SI prefix should match, except that they must also have
* the same dimensionality sign, such that we don't merge numerator and denominator.
*/
boolean isCompatibleWith(SingleUnitImpl other) {
return (compareTo(other) == 0);
}
public String getSimpleUnit() {
return simpleUnit;
}
public void setSimpleUnit(int simpleUnitIndex, String[] simpleUnits) {
this.index = simpleUnitIndex;
this.simpleUnit = simpleUnits[simpleUnitIndex];
}
public int getDimensionality() {
return dimensionality;
}
public void setDimensionality(int dimensionality) {
this.dimensionality = dimensionality;
}
public MeasureUnit.SIPrefix getSiPrefix() {
return siPrefix;
}
public void setSiPrefix(MeasureUnit.SIPrefix siPrefix) {
this.siPrefix = siPrefix;
}
public int getIndex() {
return index;
}
}

View file

@ -0,0 +1,92 @@
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.UResourceBundle;
import java.util.ArrayList;
/**
* Responsible for all units data operations (retriever, analysis, extraction certain data ... etc.).
*/
class UnitsData {
private volatile static String[] simpleUnits = null;
public static String[] getSimpleUnits() {
if (simpleUnits != null) {
return simpleUnits;
}
// Read simple units
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
SimpleUnitIdentifiersSink sink = new SimpleUnitIdentifiersSink();
resource.getAllItemsWithFallback("convertUnits", sink);
simpleUnits = sink.simpleUnits;
return simpleUnits;
}
public static class SimpleUnitIdentifiersSink extends UResource.Sink {
String[] simpleUnits = null;
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert key.toString().equals(Constants.CONVERSION_UNIT_TABLE_NAME);
assert value.getType() == UResourceBundle.TABLE;
UResource.Table simpleUnitsTable = value.getTable();
ArrayList<String> simpleUnits = new ArrayList<>();
for (int i = 0; simpleUnitsTable.getKeyAndValue(i, key, value); i++) {
if (key.toString().equals("kilogram")) {
// For parsing, we use "gram", the prefixless metric mass unit. We
// thus ignore the SI Base Unit of Mass: it exists due to being the
// mass conversion target unit, but not needed for MeasureUnit
// parsing.
continue;
}
simpleUnits.add(key.toString());
}
this.simpleUnits = simpleUnits.toArray(new String[0]);
}
}
/**
* Contains all the needed constants.
*/
public static class Constants {
// Trie value offset for simple units, e.g. "gram", "nautical-mile",
// "fluid-ounce-imperial".
public static final int kSimpleUnitOffset = 512;
// Trie value offset for powers like "square-", "cubic-", "pow2-" etc.
public static final int kPowerPartOffset = 256;
// Trie value offset for "per-".
public final static int kInitialCompoundPartOffset = 192;
// Trie value offset for compound parts, e.g. "-per-", "-", "-and-".
public final static int kCompoundPartOffset = 128;
// Trie value offset for SI Prefixes. This is big enough to ensure we only
// insert positive integers into the trie.
public static final int kSIPrefixOffset = 64;
/* Tables Names*/
public static final String CONVERSION_UNIT_TABLE_NAME = "convertUnits";
public static final String UNIT_PREFERENCE_TABLE_NAME = "unitPreferenceData";
public static final String CATEGORY_TABLE_NAME = "unitQuantities";
public static final String DEFAULT_REGION = "001";
public static final String DEFAULT_USAGE = "default";
}
}

View file

@ -14,10 +14,12 @@ import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.ibm.icu.impl.CollectionSet;
@ -25,8 +27,12 @@ import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.SingleUnitImpl;
import com.ibm.icu.text.UnicodeSet;
/**
* A unit such as length, mass, volume, currency, etc. A unit is
* coupled with a numeric amount to produce a Measure. MeasureUnit objects are immutable.
@ -48,6 +54,7 @@ public class MeasureUnit implements Serializable {
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.
*/
@ -55,12 +62,255 @@ public class MeasureUnit implements Serializable {
protected final String type;
/**
* If subType set to null, measureUnitImpl is in use instead of type and subType.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
protected final String subType;
/**
* Used by new draft APIs in ICU 68.
*
* @internal
*/
private MeasureUnitImpl measureUnitImpl = null;
/**
* Enumeration for unit complexity. There are three levels:
* <p>
* - SINGLE: A single unit, optionally with a power and/or SI prefix. Examples: hectare,
* square-kilometer, kilojoule, one-per-second.
* - COMPOUND: A unit composed of the product of multiple single units. Examples:
* meter-per-second, kilowatt-hour, kilogram-meter-per-square-second.
* - 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.
* <p>
* The complexity determines which operations are available. For example, you cannot set the power
* or SI prefix of a compound unit.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public enum Complexity {
/**
* A single unit, like kilojoule.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
SINGLE,
/**
* A compound unit, like meter-per-second.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
COMPOUND,
/**
* A mixed unit, like hour-and-minute.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
MIXED
}
/**
* Enumeration for SI prefixes, such as "kilo".
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public enum SIPrefix {
/**
* SI prefix: yotta, 10^24.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
YOTTA(24, "yotta"),
/**
* SI prefix: zetta, 10^21.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
ZETTA(21, "zetta"),
/**
* SI prefix: exa, 10^18.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
EXA(18, "exa"),
/**
* SI prefix: peta, 10^15.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
PETA(15, "peta"),
/**
* SI prefix: tera, 10^12.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
TERA(12, "tera"),
/**
* SI prefix: giga, 10^9.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
GIGA(9, "giga"),
/**
* SI prefix: mega, 10^6.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
MEGA(6, "mega"),
/**
* SI prefix: kilo, 10^3.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
KILO(3, "kilo"),
/**
* SI prefix: hecto, 10^2.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
HECTO(2, "hecto"),
/**
* SI prefix: deka, 10^1.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
DEKA(1, "deka"),
/**
* The absence of an SI prefix.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
ONE(0, ""),
/**
* SI prefix: deci, 10^-1.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
DECI(-1, "deci"),
/**
* SI prefix: centi, 10^-2.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
CENTI(-2, "centi"),
/**
* SI prefix: milli, 10^-3.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
MILLI(-3, "milli"),
/**
* SI prefix: micro, 10^-6.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
MICRO(-6, "micro"),
/**
* SI prefix: nano, 10^-9.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
NANO(-9, "nano"),
/**
* SI prefix: pico, 10^-12.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
PICO(-12, "pico"),
/**
* SI prefix: femto, 10^-15.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
FEMTO(-15, "femto"),
/**
* SI prefix: atto, 10^-18.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
ATTO(-18, "atto"),
/**
* SI prefix: zepto, 10^-21.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
ZEPTO(-21, "zepto"),
/**
* SI prefix: yocto, 10^-24.
*
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
YOCTO(-24, "yocto");
private final int siPrefixPower;
private final String identifier;
SIPrefix(int siPrefixPower, String identifier) {
this.siPrefixPower = siPrefixPower;
this.identifier = identifier;
}
public String getIdentifier() {
return identifier;
}
public int getSiPrefixPower() {
return siPrefixPower;
}
}
/**
* @internal
* @deprecated This API is ICU internal only.
@ -71,6 +321,52 @@ public class MeasureUnit implements Serializable {
this.subType = subType;
}
/**
* Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35.
* Validates and canonicalizes the identifier.
*
* Note: dimensionless <code>MeasureUnit</code> is <code>null</code>
*
* <pre>
* MeasureUnit example = MeasureUnit::forIdentifier("furlong-per-nanosecond")
* </pre>
*
* @param identifier The CLDR Sequence Unit Identifier
* @throws IllegalArgumentException if the identifier is invalid.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public static MeasureUnit forIdentifier(String identifier) {
if (identifier == null || identifier.isEmpty()) {
return NoUnit.BASE;
}
return MeasureUnitImpl.forIdentifier(identifier).build();
}
/**
* @internal
* @param measureUnitImpl
*/
public static MeasureUnit fromMeasureUnitImpl(MeasureUnitImpl measureUnitImpl) {
measureUnitImpl.serialize();
String identifier = measureUnitImpl.getIdentifier();
MeasureUnit result = MeasureUnit.findBySubType(identifier);
if (result != null) {
return result;
}
return new MeasureUnit(measureUnitImpl);
}
private MeasureUnit(MeasureUnitImpl measureUnitImpl) {
type = null;
subType = null;
this.measureUnitImpl = measureUnitImpl.clone();
}
/**
* Get the type, such as "length"
*
@ -90,7 +386,187 @@ public class MeasureUnit implements Serializable {
return subType;
}
/**
* Gets the CLDR Unit Identifier for this MeasureUnit, as defined in UTS 35.
*
* @return The string form of this unit.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public String getIdentifier() {
String result = measureUnitImpl == null ? getSubtype() : measureUnitImpl.getIdentifier();
return result == null ? "" : result;
}
/**
* Compute the complexity of the unit. See Complexity for more information.
*
* @return The unit complexity.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public Complexity getComplexity() {
if (measureUnitImpl == null) {
return MeasureUnitImpl.forIdentifier(getIdentifier()).getComplexity();
}
return measureUnitImpl.getComplexity();
}
/**
* Creates a MeasureUnit which is this SINGLE unit augmented with the specified SI prefix.
* For example, SI_PREFIX_KILO for "kilo".
* May return this if this unit already has that prefix.
* <p>
* There is sufficient locale data to format all standard SI prefixes.
* <p>
* NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an error will
* occur. For more information, see `Complexity`.
*
* @param prefix The SI prefix, from SIPrefix.
* @return A new SINGLE unit.
* @throws UnsupportedOperationException if this unit is a COMPOUND or MIXED unit.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public MeasureUnit withSIPrefix(SIPrefix prefix) {
SingleUnitImpl singleUnit = getSingleUnitImpl();
singleUnit.setSiPrefix(prefix);
return singleUnit.build();
}
/**
* Returns the current SI prefix of this SINGLE unit. For example, if the unit has the SI prefix
* "kilo", then SI_PREFIX_KILO is returned.
* <p>
* NOTE: Only works on SINGLE units. If this is a COMPOUND or MIXED unit, an error will
* occur. For more information, see `Complexity`.
*
* @return The SI prefix of this SINGLE unit, from SIPrefix.
* @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public SIPrefix getSIPrefix() {
return getSingleUnitImpl().getSiPrefix();
}
/**
* 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.
* For more information, see `Complexity`.
*
* @return The dimensionality (power) of this simple unit.
* @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public int getDimensionality() {
return getSingleUnitImpl().getDimensionality();
}
/**
* 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.
* For more information, see `Complexity`.
*
* @param dimensionality The dimensionality (power).
* @return A new SINGLE unit.
* @throws UnsupportedOperationException if the unit is COMPOUND or MIXED.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public MeasureUnit withDimensionality(int dimensionality) {
SingleUnitImpl singleUnit = getSingleUnitImpl();
singleUnit.setDimensionality(dimensionality);
return singleUnit.build();
}
/**
* 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.
* <p>
* NOTE: Only works on SINGLE and COMPOUND units. If this is a MIXED unit, an error will
* occur. For more information, see `Complexity`.
*
* @return The reciprocal of the target unit.
* @throws UnsupportedOperationException if the unit is MIXED.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public MeasureUnit reciprocal() {
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
* 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
* unit "kilowatt-hour-per-day" is returned.
* <p>
* 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, see `Complexity`.
*
* @param other The MeasureUnit to multiply with the target.
* @return The product of the target unit with the provided unit.
* @throws UnsupportedOperationException if the unit is MIXED.
* @draft ICU 68
* @provisional This API might change or be removed in a future release.
*/
public MeasureUnit product(MeasureUnit other) {
MeasureUnitImpl implCopy = getCopyOfMeasureUnitImpl();
if (other == null /* dimensionless */) {
return implCopy.build();
}
final MeasureUnitImpl otherImplRef = other.getMayBeReferenceOfMeasureUnitImpl();
if (implCopy.getComplexity() == Complexity.MIXED || otherImplRef.getComplexity() == Complexity.MIXED) {
throw new UnsupportedOperationException();
}
for (SingleUnitImpl singleUnit :
otherImplRef.getSingleUnits()) {
implCopy.appendSingleUnit(singleUnit);
}
return implCopy.build();
}
/**
* 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",
* "kilogram", and "one-per-second".
* - Given "hour+minute+second", three units will be returned: "hour", "minute",
* and "second".
* <p>
* If this is a SINGLE unit, a list of length 1 will be returned.
*
* @return An unmodifiable list of single units
* @internal ICU 68 Technology Preview
* @provisional This API might change or be removed in a future release.
*/
public List<MeasureUnit> splitToSingleUnits() {
final ArrayList<SingleUnitImpl> singleUnits = getMayBeReferenceOfMeasureUnitImpl().getSingleUnits();
List<MeasureUnit> result = new ArrayList<>(singleUnits.size());
for (SingleUnitImpl singleUnit : singleUnits) {
result.add(singleUnit.build());
}
return result;
}
/**
* {@inheritDoc}
@ -115,8 +591,8 @@ public class MeasureUnit implements Serializable {
if (!(rhs instanceof MeasureUnit)) {
return false;
}
MeasureUnit c = (MeasureUnit) rhs;
return type.equals(c.type) && subType.equals(c.subType);
return this.getIdentifier().equals(((MeasureUnit) rhs).getIdentifier());
}
/**
@ -1546,6 +2022,39 @@ public class MeasureUnit implements Serializable {
return new MeasureUnitProxy(type, subType);
}
/**
*
* @return this object as a SingleUnitImpl.
* @throws UnsupportedOperationException if this object could not be converted to a single unit.
*/
private SingleUnitImpl getSingleUnitImpl() {
if (measureUnitImpl == null) {
return MeasureUnitImpl.forIdentifier(getIdentifier()).getSingleUnitImpl();
}
return measureUnitImpl.getSingleUnitImpl();
}
/**
*
* @return this object in a MeasureUnitImpl form.
*/
private MeasureUnitImpl getCopyOfMeasureUnitImpl() {
return this.measureUnitImpl == null ?
MeasureUnitImpl.forIdentifier(getIdentifier()) :
this.measureUnitImpl.clone();
}
/**
*
* @return this object in a MeasureUnitImpl form.
*/
private MeasureUnitImpl getMayBeReferenceOfMeasureUnitImpl(){
return this.measureUnitImpl == null ?
MeasureUnitImpl.forIdentifier(getIdentifier()) :
this.measureUnitImpl;
}
static final class MeasureUnitProxy implements Externalizable {
private static final long serialVersionUID = -3910681415330989598L;
@ -1586,4 +2095,4 @@ public class MeasureUnit implements Serializable {
return MeasureUnit.internalGetInstance(type, subType);
}
}
}
}

View file

@ -8,33 +8,6 @@
*/
package com.ibm.icu.dev.test.format;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
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;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.serializable.FormatHandler;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
@ -45,13 +18,17 @@ import com.ibm.icu.text.MeasureFormat;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
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.*;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.*;
import java.lang.reflect.Field;
import java.text.FieldPosition;
import java.text.ParseException;
import java.util.*;
/**
* See https://sites.google.com/site/icusite/processes/release/tasks/standards?pli=1
@ -3485,4 +3462,405 @@ public class MeasureUnitTest extends TestFmwk {
fmt = MeasureFormat.getInstance(ULocale.forLanguageTag("da"), FormatWidth.NUMERIC);
Assert.assertEquals("2.03,877", fmt.formatMeasures(fhours, fminutes));
}
@Test
public void TestIdentifiers() {
class TestCase {
final String id;
final String normalized;
TestCase(String id, String normalized) {
this.id = id;
this.normalized = normalized;
}
}
TestCase cases[] = {
// Correctly normalized identifiers should not change
new TestCase("square-meter-per-square-meter", "square-meter-per-square-meter"),
new TestCase("kilogram-meter-per-square-meter-square-second",
"kilogram-meter-per-square-meter-square-second"),
new TestCase("square-mile-and-square-foot", "square-mile-and-square-foot"),
new TestCase("square-foot-and-square-mile", "square-foot-and-square-mile"),
new TestCase("per-cubic-centimeter", "per-cubic-centimeter"),
new TestCase("per-kilometer", "per-kilometer"),
// Normalization of power and per
new TestCase(
"pow2-foot-and-pow2-mile", "square-foot-and-square-mile"),
new TestCase(
"gram-square-gram-per-dekagram", "cubic-gram-per-dekagram"),
new TestCase(
"kilogram-per-meter-per-second", "kilogram-per-meter-second"),
// TODO(ICU-20920): Add more test cases once the proper ranking is available.
};
for (TestCase testCase : cases) {
MeasureUnit unit = MeasureUnit.forIdentifier(testCase.id);
final String actual = unit.getIdentifier();
assertEquals(testCase.id, testCase.normalized, actual);
}
assertEquals("for empty identifiers, the MeasureUnit will be null",
null, MeasureUnit.forIdentifier(""));
}
@Test
public void TestInvalidIdentifiers() {
final String inputs[] = {
"kilo",
"kilokilo",
"onekilo",
"meterkilo",
"meter-kilo",
"k",
"meter-",
"meter+",
"-meter",
"+meter",
"-kilometer",
"+kilometer",
"-pow2-meter",
"+pow2-meter",
"p2-meter",
"p4-meter",
"+",
"-",
"-mile",
"-and-mile",
"-per-mile",
"one",
"one-one",
"one-per-mile",
"one-per-cubic-centimeter",
"square--per-meter",
"metersecond", // Must have compound part in between single units
// Negative powers not supported in mixed units yet. TODO(CLDR-13701).
"per-hour-and-hertz",
"hertz-and-per-hour",
// Compound units not supported in mixed units yet. TODO(CLDR-13700).
"kilonewton-meter-and-newton-meter",
};
for (String input : inputs) {
try {
MeasureUnit.forIdentifier(input);
Assert.fail("An IllegalArgumentException must be thrown");
} catch (IllegalArgumentException e) {
continue;
}
}
}
@Test
public void TestCompoundUnitOperations() {
MeasureUnit.forIdentifier("kilometer-per-second-joule");
MeasureUnit kilometer = MeasureUnit.KILOMETER;
MeasureUnit cubicMeter = MeasureUnit.CUBIC_METER;
MeasureUnit meter = kilometer.withSIPrefix(MeasureUnit.SIPrefix.ONE);
MeasureUnit centimeter1 = kilometer.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
MeasureUnit centimeter2 = meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
MeasureUnit cubicDecimeter = cubicMeter.withSIPrefix(MeasureUnit.SIPrefix.DECI);
verifySingleUnit(kilometer, MeasureUnit.SIPrefix.KILO, 1, "kilometer");
verifySingleUnit(meter, MeasureUnit.SIPrefix.ONE, 1, "meter");
verifySingleUnit(centimeter1, MeasureUnit.SIPrefix.CENTI, 1, "centimeter");
verifySingleUnit(centimeter2, MeasureUnit.SIPrefix.CENTI, 1, "centimeter");
verifySingleUnit(cubicDecimeter, MeasureUnit.SIPrefix.DECI, 3, "cubic-decimeter");
assertTrue("centimeter equality", centimeter1.equals( centimeter2));
assertTrue("kilometer inequality", !centimeter1.equals( kilometer));
MeasureUnit squareMeter = meter.withDimensionality(2);
MeasureUnit overCubicCentimeter = centimeter1.withDimensionality(-3);
MeasureUnit quarticKilometer = kilometer.withDimensionality(4);
MeasureUnit overQuarticKilometer1 = kilometer.withDimensionality(-4);
verifySingleUnit(squareMeter, MeasureUnit.SIPrefix.ONE, 2, "square-meter");
verifySingleUnit(overCubicCentimeter, MeasureUnit.SIPrefix.CENTI, -3, "per-cubic-centimeter");
verifySingleUnit(quarticKilometer, MeasureUnit.SIPrefix.KILO, 4, "pow4-kilometer");
verifySingleUnit(overQuarticKilometer1, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
assertTrue("power inequality", quarticKilometer != overQuarticKilometer1);
MeasureUnit overQuarticKilometer2 = quarticKilometer.reciprocal();
MeasureUnit overQuarticKilometer3 = kilometer.product(kilometer)
.product(kilometer)
.product(kilometer)
.reciprocal();
MeasureUnit overQuarticKilometer4 = meter.withDimensionality(4)
.reciprocal()
.withSIPrefix(MeasureUnit.SIPrefix.KILO);
verifySingleUnit(overQuarticKilometer2, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
verifySingleUnit(overQuarticKilometer3, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
verifySingleUnit(overQuarticKilometer4, MeasureUnit.SIPrefix.KILO, -4, "per-pow4-kilometer");
assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer2));
assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer3));
assertTrue("reciprocal equality", overQuarticKilometer1.equals(overQuarticKilometer4));
MeasureUnit kiloSquareSecond = MeasureUnit.SECOND
.withDimensionality(2).withSIPrefix(MeasureUnit.SIPrefix.KILO);
MeasureUnit meterSecond = meter.product(kiloSquareSecond);
MeasureUnit cubicMeterSecond1 = meter.withDimensionality(3).product(kiloSquareSecond);
MeasureUnit centimeterSecond1 = meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI).product(kiloSquareSecond);
MeasureUnit secondCubicMeter = kiloSquareSecond.product(meter.withDimensionality(3));
MeasureUnit secondCentimeter = kiloSquareSecond.product(meter.withSIPrefix(MeasureUnit.SIPrefix.CENTI));
MeasureUnit secondCentimeterPerKilometer = secondCentimeter.product(kilometer.reciprocal());
verifySingleUnit(kiloSquareSecond, MeasureUnit.SIPrefix.KILO, 2, "square-kilosecond");
String meterSecondSub[] = {
"meter", "square-kilosecond"
};
verifyCompoundUnit(meterSecond, "meter-square-kilosecond",
meterSecondSub, meterSecondSub.length);
String cubicMeterSecond1Sub[] = {
"cubic-meter", "square-kilosecond"
};
verifyCompoundUnit(cubicMeterSecond1, "cubic-meter-square-kilosecond",
cubicMeterSecond1Sub, cubicMeterSecond1Sub.length);
String centimeterSecond1Sub[] = {
"centimeter", "square-kilosecond"
};
verifyCompoundUnit(centimeterSecond1, "centimeter-square-kilosecond",
centimeterSecond1Sub, centimeterSecond1Sub.length);
String secondCubicMeterSub[] = {
"cubic-meter", "square-kilosecond"
};
verifyCompoundUnit(secondCubicMeter, "cubic-meter-square-kilosecond",
secondCubicMeterSub, secondCubicMeterSub.length);
String secondCentimeterSub[] = {
"centimeter", "square-kilosecond"
};
verifyCompoundUnit(secondCentimeter, "centimeter-square-kilosecond",
secondCentimeterSub, secondCentimeterSub.length);
String secondCentimeterPerKilometerSub[] = {
"centimeter", "square-kilosecond", "per-kilometer"
};
verifyCompoundUnit(secondCentimeterPerKilometer, "centimeter-square-kilosecond-per-kilometer",
secondCentimeterPerKilometerSub, secondCentimeterPerKilometerSub.length);
assertTrue("reordering equality", cubicMeterSecond1.equals(secondCubicMeter));
assertTrue("additional simple units inequality", !secondCubicMeter.equals(secondCentimeter));
// Don't allow get/set power or SI prefix on compound units
try {
meterSecond.getDimensionality();
fail("UnsupportedOperationException must be thrown");
} catch (UnsupportedOperationException e) {
// Expecting an exception to be thrown
}
try {
meterSecond.withDimensionality(3);
fail("UnsupportedOperationException must be thrown");
} catch (UnsupportedOperationException e) {
// Expecting an exception to be thrown
}
try {
meterSecond.getSIPrefix();
fail("UnsupportedOperationException must be thrown");
} catch (UnsupportedOperationException e) {
// Expecting an exception to be thrown
}
try {
meterSecond.withSIPrefix(MeasureUnit.SIPrefix.CENTI);
fail("UnsupportedOperationException must be thrown");
} catch (UnsupportedOperationException e) {
// Expecting an exception to be thrown
}
MeasureUnit footInch = MeasureUnit.forIdentifier("foot-and-inch");
MeasureUnit inchFoot = MeasureUnit.forIdentifier("inch-and-foot");
String footInchSub[] = {
"foot", "inch"
};
verifyMixedUnit(footInch, "foot-and-inch",
footInchSub, footInchSub.length);
String inchFootSub[] = {
"inch", "foot"
};
verifyMixedUnit(inchFoot, "inch-and-foot",
inchFootSub, inchFootSub.length);
assertTrue("order matters inequality", !footInch.equals(inchFoot));
MeasureUnit dimensionless = NoUnit.BASE;
MeasureUnit dimensionless2 = MeasureUnit.forIdentifier("");
assertEquals("dimensionless equality", dimensionless, dimensionless2);
// We support starting from an "identity" MeasureUnit and then combining it
// with others via product:
MeasureUnit kilometer2 = kilometer.product(dimensionless);
verifySingleUnit(kilometer2, MeasureUnit.SIPrefix.KILO, 1, "kilometer");
assertTrue("kilometer equality", kilometer.equals(kilometer2));
// Test out-of-range powers
MeasureUnit power15 = MeasureUnit.forIdentifier("pow15-kilometer");
verifySingleUnit(power15, MeasureUnit.SIPrefix.KILO, 15, "pow15-kilometer");
try {
MeasureUnit.forIdentifier("pow16-kilometer");
fail("An IllegalArgumentException must be thrown");
} catch (IllegalArgumentException e) {
// Expecting an exception to be thrown
}
try {
power15.product(kilometer);
fail("An IllegalArgumentException must be thrown");
} catch (IllegalArgumentException e) {
// Expecting an exception to be thrown
}
MeasureUnit powerN15 = MeasureUnit.forIdentifier("per-pow15-kilometer");
verifySingleUnit(powerN15, MeasureUnit.SIPrefix.KILO, -15, "per-pow15-kilometer");
try {
MeasureUnit.forIdentifier("per-pow16-kilometer");
fail("An IllegalArgumentException must be thrown");
} catch (IllegalArgumentException e) {
// Expecting an exception to be thrown
}
try {
powerN15.product(overQuarticKilometer1);
fail("An IllegalArgumentException must be thrown");
} catch (IllegalArgumentException e) {
// Expecting an exception to be thrown
}
}
@Test
public void TestDimensionlessBehaviour() {
MeasureUnit dimensionless = MeasureUnit.forIdentifier("");
MeasureUnit dimensionless2 = NoUnit.BASE;
MeasureUnit dimensionless3 = null;
MeasureUnit dimensionless4 = MeasureUnit.forIdentifier(null);
assertEquals("dimensionless must be equals", dimensionless, dimensionless2);
assertEquals("dimensionless must be equals", dimensionless2, dimensionless3);
assertEquals("dimensionless must be equals", dimensionless3, dimensionless4);
// product(dimensionless)
MeasureUnit mile = MeasureUnit.MILE;
mile = mile.product(dimensionless);
verifySingleUnit(mile, MeasureUnit.SIPrefix.ONE, 1, "mile");
}
private void verifySingleUnit(MeasureUnit singleMeasureUnit, MeasureUnit.SIPrefix prefix, int power, String identifier) {
assertEquals(identifier + ": SI prefix", prefix, singleMeasureUnit.getSIPrefix());
assertEquals(identifier + ": Power", power, singleMeasureUnit.getDimensionality());
assertEquals(identifier + ": Identifier", identifier, singleMeasureUnit.getIdentifier());
assertTrue(identifier + ": Constructor", singleMeasureUnit.equals(MeasureUnit.forIdentifier(identifier)));
assertEquals(identifier + ": Complexity", MeasureUnit.Complexity.SINGLE, singleMeasureUnit.getComplexity());
}
// Kilogram is a "base unit", although it's also "gram" with a kilo- prefix.
// This tests that it is handled in the preferred manner.
@Test
public void TestKilogramIdentifier() {
// SI unit of mass
MeasureUnit kilogram = MeasureUnit.forIdentifier("kilogram");
// Metric mass unit
MeasureUnit gram = MeasureUnit.forIdentifier("gram");
// Microgram: still a built-in type
MeasureUnit microgram = MeasureUnit.forIdentifier("microgram");
// Nanogram: not a built-in type at this time
MeasureUnit nanogram = MeasureUnit.forIdentifier("nanogram");
assertEquals("parsed kilogram equals built-in kilogram", MeasureUnit.KILOGRAM.getType(),
kilogram.getType());
assertEquals("parsed kilogram equals built-in kilogram", MeasureUnit.KILOGRAM.getSubtype(),
kilogram.getSubtype());
assertEquals("parsed gram equals built-in gram", MeasureUnit.GRAM.getType(), gram.getType());
assertEquals("parsed gram equals built-in gram", MeasureUnit.GRAM.getSubtype(),
gram.getSubtype());
assertEquals("parsed microgram equals built-in microgram", MeasureUnit.MICROGRAM.getType(),
microgram.getType());
assertEquals("parsed microgram equals built-in microgram", MeasureUnit.MICROGRAM.getSubtype(),
microgram.getSubtype());
assertEquals("nanogram", null, nanogram.getType());
assertEquals("nanogram", "nanogram", nanogram.getIdentifier());
assertEquals("prefix of kilogram", MeasureUnit.SIPrefix.KILO, kilogram.getSIPrefix());
assertEquals("prefix of gram", MeasureUnit.SIPrefix.ONE, gram.getSIPrefix());
assertEquals("prefix of microgram", MeasureUnit.SIPrefix.MICRO, microgram.getSIPrefix());
assertEquals("prefix of nanogram", MeasureUnit.SIPrefix.NANO, nanogram.getSIPrefix());
MeasureUnit tmp = kilogram.withSIPrefix(MeasureUnit.SIPrefix.MILLI);
assertEquals("Kilogram + milli should be milligram, got: " + tmp.getIdentifier(),
MeasureUnit.MILLIGRAM.getIdentifier(), tmp.getIdentifier());
}
private void verifyCompoundUnit(
MeasureUnit unit,
String identifier,
String subIdentifiers[],
int subIdentifierCount) {
assertEquals(identifier + ": Identifier",
identifier,
unit.getIdentifier());
assertTrue(identifier + ": Constructor",
unit.equals(MeasureUnit.forIdentifier(identifier)));
assertEquals(identifier + ": Complexity",
MeasureUnit.Complexity.COMPOUND,
unit.getComplexity());
List<MeasureUnit> subUnits = unit.splitToSingleUnits();
assertEquals(identifier + ": Length", subIdentifierCount, subUnits.size());
for (int i = 0; ; i++) {
if (i >= subIdentifierCount || i >= subUnits.size()) break;
assertEquals(identifier + ": Sub-unit #" + i,
subIdentifiers[i],
subUnits.get(i).getIdentifier());
assertEquals(identifier + ": Sub-unit Complexity",
MeasureUnit.Complexity.SINGLE,
subUnits.get(i).getComplexity());
}
}
private void verifyMixedUnit(
MeasureUnit unit,
String identifier,
String subIdentifiers[],
int subIdentifierCount) {
assertEquals(identifier + ": Identifier",
identifier,
unit.getIdentifier());
assertTrue(identifier + ": Constructor",
unit.equals(MeasureUnit.forIdentifier(identifier)));
assertEquals(identifier + ": Complexity",
MeasureUnit.Complexity.MIXED,
unit.getComplexity());
List<MeasureUnit> subUnits = unit.splitToSingleUnits();
assertEquals(identifier + ": Length", subIdentifierCount, subUnits.size());
for (int i = 0; ; i++) {
if (i >= subIdentifierCount || i >= subUnits.size()) break;
assertEquals(identifier + ": Sub-unit #" + i,
subIdentifiers[i],
subUnits.get(i).getIdentifier());
}
}
}