--- /dev/null
+// © 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);
+ }
+ }
+}
--- /dev/null
+// © 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;
+ }
+
+}
--- /dev/null
+// © 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";
+ }
+}
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;
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.
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.
*/
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.
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"
*
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}
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());
}
/**
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;
return MeasureUnit.internalGetInstance(type, subType);
}
}
-}
+}
\ No newline at end of file
*/
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;
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
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());
+ }
+ }
}