blob: db98db0705a74645839e65f842a6ad438d7471d3 [file] [log] [blame]
// © 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);
}
}
}