dimensions;
/**
* Creates the {@code UnitsDatabase}.
*
* @since 2019-01-10
* @since v0.1.0
*/
public UnitsDatabase() {
this.units = new HashMap<>();
this.prefixes = new HashMap<>();
this.dimensions = new HashMap<>();
}
/**
* Adds all units from a file, using data from the database to parse them.
*
* Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by
* any number of tab characters.
*
*
* Allowed exceptions:
*
* - Any line that begins with the '#' character is considered a comment and ignored.
* - Blank lines are also ignored
* - If an expression consists of a single exclamation point, instead of parsing it, this method will search the
* database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
* initial units and ensure that the database contains them.
*
*
* @param file
* file to read
* @throws IllegalArgumentException
* if the file cannot be parsed, found or read
* @throws NullPointerException
* if file is null
* @since 2019-01-13
* @since v0.1.0
*/
public void addAllFromFile(final File file) {
Objects.requireNonNull(file, "file must not be null.");
try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) {
// while the reader has lines to read, read a line, then parse it, then add it
long lineCounter = 0;
while (reader.ready()) {
final String line = reader.readLine();
lineCounter++;
// ignore lines that start with a # sign - they're comments
if (line.startsWith("#") || line.isEmpty()) {
continue;
}
// divide line into name and expression
final String[] parts = line.split("\t");
if (parts.length < 2)
throw new IllegalArgumentException(String.format(
"Lines must consist of a unit name and its definition, separated by tab(s) (line %d).",
lineCounter));
final String name = parts[0];
final String expression = parts[parts.length - 1];
if (name.endsWith(" ")) {
System.err.printf("Warning - line %d's unit name ends in a space", lineCounter);
}
// if expression is "!", search for an existing unit
// if no unit found, throw an error
if (expression.equals("!")) {
if (!this.containsUnitName(name))
throw new IllegalArgumentException(
String.format("! used but no unit found (line %d).", lineCounter));
} else {
if (name.endsWith("-")) {
final UnitPrefix prefix;
try {
prefix = this.getPrefixFromExpression(expression);
} catch (final IllegalArgumentException e) {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
this.addPrefix(name.substring(0, name.length() - 1), prefix);
} else {
// it's a unit, get the unit
final Unit unit;
try {
unit = this.getUnitFromExpression(expression);
} catch (final IllegalArgumentException e) {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
AbstractUnit.incrementUnitCounter();
if (unit instanceof BaseUnit) {
AbstractUnit.incrementBaseUnitCounter();
}
this.addUnit(name, unit);
}
}
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
} catch (final IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
}
}
/**
* Adds a unit dimension to the database.
*
* @param name
* dimension's name
* @param dimension
* dimension to add
* @throws NullPointerException
* if name or dimension is null
* @since 2019-03-14
*/
public void addDimension(final String name, final UnitDimension dimension) {
this.dimensions.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(dimension, "dimension must not be null."));
}
/**
* Adds a unit prefix to the database.
*
* @param name
* prefix's name
* @param prefix
* prefix to add
* @throws NullPointerException
* if name or prefix is null
* @since 2019-01-14
* @since v0.1.0
*/
public void addPrefix(final String name, final UnitPrefix prefix) {
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(prefix, "prefix must not be null."));
}
/**
* Adds a unit to the database.
*
* @param name
* unit's name
* @param unit
* unit to add
* @throws NullPointerException
* if unit is null
* @since 2019-01-10
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
this.units.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(unit, "unit must not be null."));
}
/**
* Tests if the database has a unit dimension with this name.
*
* @param name
* name to test
* @return if database contains name
* @since 2019-03-14
*/
public boolean containsDimensionName(final String name) {
return this.dimensions.containsKey(name);
}
/**
* Tests if the database has a unit with this name, ignoring prefixes
*
* @param name
* name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
*/
public boolean containsPrefixlessUnitName(final String name) {
return this.units.containsKey(name);
}
/**
* Tests if the database has a unit prefix with this name.
*
* @param name
* name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
*/
public boolean containsPrefixName(final String name) {
return this.prefixes.containsKey(name);
}
/**
* Tests if the database has a unit with this name, taking prefixes into consideration
*
* @param name
* name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
*/
public boolean containsUnitName(final String name) {
// check for prefixes
for (final String prefixName : this.prefixNameSet()) {
if (name.startsWith(prefixName))
if (this.containsUnitName(name.substring(prefixName.length())))
return true;
}
return this.units.containsKey(name);
}
/**
* @return an immutable set of all of the dimension names in this database.
* @since 2019-03-14
*/
public Set dimensionNameSet() {
return Collections.unmodifiableSet(this.dimensions.keySet());
}
/**
* Gets a unit dimension from the database using its name.
*
* @param name
* dimension's name
* @return dimension
* @since 2019-03-14
*/
public UnitDimension getDimension(final String name) {
return this.dimensions.get(name);
}
/**
* Gets a unit prefix from the database from its name
*
* @param name
* prefix's name
* @return prefix
* @since 2019-01-10
* @since v0.1.0
*/
public UnitPrefix getPrefix(final String name) {
return this.prefixes.get(name);
}
/**
* Gets a unit prefix from a prefix expression
*
* Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of
* another prefix
*
*
* @param expression
* expression to input
* @return prefix
* @throws IllegalArgumentException
* if expression cannot be parsed
* @throws NullPointerException
* if any argument is null
* @since 2019-01-14
* @since v0.1.0
*/
public UnitPrefix getPrefixFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
try {
return new DefaultUnitPrefix(Double.parseDouble(expression));
} catch (final NumberFormatException e) {
if (expression.contains("^")) {
final String[] baseAndExponent = expression.split("\\^");
final double base;
try {
base = Double.parseDouble(baseAndExponent[0]);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Base of exponientation must be a number.");
}
final int exponent;
try {
exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Exponent must be an integer.");
}
return new DefaultUnitPrefix(Math.pow(base, exponent));
} else {
if (!this.containsPrefixName(expression))
throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\".");
return this.getPrefix(expression);
}
}
}
/**
* Gets a unit from the database from its name, ignoring prefixes.
*
* @param name
* unit's name
* @return unit
* @since 2019-01-10
* @since v0.1.0
*/
public Unit getPrefixlessUnit(final String name) {
return this.units.get(name);
}
/**
* Gets a unit from the database from its name, looking for prefixes.
*
* @param name
* unit's name
* @return unit
* @since 2019-01-10
* @since v0.1.0
*/
public Unit getUnit(final String name) {
if (name.contains("^")) {
final String[] baseAndExponent = name.split("\\^");
LinearUnit base;
try {
base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
} catch (final NumberFormatException e2) {
final Unit unit = this.getUnit(baseAndExponent[0]);
if (unit instanceof LinearUnit) {
base = (LinearUnit) unit;
} else if (unit instanceof BaseUnit) {
base = ((BaseUnit) unit).asLinearUnit();
} else
throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
}
final int exponent;
try {
exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Exponent must be an integer.");
}
final LinearUnit exponentiated = base.toExponent(exponent);
if (exponentiated.getConversionFactor() == 1)
return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
else
return exponentiated;
} else {
for (final String prefixName : this.prefixNameSet()) {
// check for a prefix
if (name.startsWith(prefixName)) {
// prefix found! Make sure what comes after it is actually a unit!
final String prefixless = name.substring(prefixName.length());
if (this.containsUnitName(prefixless)) {
// yep, it's a proper prefix! Get the unit!
final Unit unit = this.getUnit(prefixless);
final UnitPrefix prefix = this.getPrefix(prefixName);
// Prefixes only work with linear and base units, so make sure it's one of those
if (unit instanceof LinearUnit) {
final LinearUnit linearUnit = (LinearUnit) unit;
return linearUnit.times(prefix.getMultiplier());
} else if (unit instanceof BaseUnit) {
final BaseUnit baseUnit = (BaseUnit) unit;
return baseUnit.times(prefix.getMultiplier());
}
}
}
}
return this.units.get(name);
}
}
/**
* Uses the database's unit data to parse an expression into a unit
*
* The expression is a series of any of the following:
*
* - The name of a unit, which multiplies or divides the result based on preceding operators
* - The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each
* other is equivalent to multiplication)
* - The operator '^' which exponentiates. Exponents must be integers.
* - A number which is multiplied or divided
*
* This method only works with linear units.
*
* @param expression
* expression to parse
* @throws IllegalArgumentException
* if the expression cannot be parsed
* @throws NullPointerException
* if any argument is null
* @since 2019-01-07
* @since v0.1.0
*/
public Unit getUnitFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// parse the expression
// start with an "empty" unit then apply operations on it
LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
boolean dividing = false;
// if I'm just creating an alias, just create one instead of going through the parsing process
if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/")
&& !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) {
try {
return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression));
} catch (final NumberFormatException e) {
if (!this.containsUnitName(expression))
throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\".");
return this.getUnit(expression);
}
}
// \\* means "asterisk", * is reserved
for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^")
.replaceAll("\\^ ", "\\^").split(" ")) {
if ("".equals(part)) {
continue;
}
// "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing
if ("*".equals(part)) {
continue;
}
// When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next
// thing
if ("/".equals(part) || "per".equals(part)) {
dividing = true;
continue;
}
try {
final double partAsNumber = Double.parseDouble(part); // if this works, it's a number
// this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does
try {
if (dividing) {
unit = unit.dividedBy(partAsNumber);
dividing = false;
} else {
unit = unit.times(partAsNumber);
}
} catch (final Exception e) {
throw new AssertionError(e);
}
} catch (final NumberFormatException e) {
// it's a unit, try that
if (part.contains("(") && part.endsWith(")")) {
// the unitsfile is looking for a nonlinear unit
final String[] unitAndValue = part.split("\\(");
// this will work because I've checked that it contains a (
final String unitName = unitAndValue[0];
final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1];
final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1);
final double value;
// try to get the value - else throw an error
try {
value = Double.parseDouble(valueString);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Unparseable value " + valueString);
}
// get this unit in a linear form
if (!this.containsPrefixlessUnitName(unitName))
throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
final Unit partUnit = this.getPrefixlessUnit(unitName);
final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value));
// finally, add it to the expression
if (dividing) {
unit = unit.dividedBy(multiplier);
dividing = false;
} else {
unit = unit.times(multiplier);
}
} else {
// check for exponientation
if (part.contains("^")) {
final String[] valueAndExponent = part.split("\\^");
// this will always work because of the contains check
final String valueString = valueAndExponent[0];
final String exponentString = valueAndExponent[valueAndExponent.length - 1];
LinearUnit value;
// first, try to get the value
try {
final double valueAsNumber = Double.parseDouble(valueString);
value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber);
} catch (final NumberFormatException e2) {
// look for a unit
if (!this.containsUnitName(valueString))
throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\".");
final Unit valueUnit = this.getUnit(valueString);
// try to turn the value into a linear unit
if (valueUnit instanceof LinearUnit) {
value = (LinearUnit) valueUnit;
} else if (valueUnit instanceof BaseUnit) {
value = ((BaseUnit) valueUnit).asLinearUnit();
} else
throw new IllegalArgumentException("Only linear and base units can be exponientated.");
}
// now, try to get the exponent
final int exponent;
try {
exponent = Integer.parseInt(exponentString);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Exponents must be integers.");
}
final LinearUnit exponientated = value.toExponent(exponent);
if (dividing) {
unit = unit.dividedBy(exponientated);
dividing = false;
} else {
unit = unit.times(exponientated);
}
} else {
// no exponent - look for a unit
// the unitsfile is looking for a linear unit
if (!this.containsUnitName(part))
throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
Unit other = this.getUnit(part);
if (other instanceof BaseUnit) {
other = ((BaseUnit) other).asLinearUnit();
}
if (other instanceof LinearUnit) {
if (dividing) {
unit = unit.dividedBy((LinearUnit) other);
dividing = false;
} else {
unit = unit.times((LinearUnit) other);
}
} else
throw new IllegalArgumentException(
"Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE).");
}
}
}
}
// replace conversion-factor-1 linear units with base units
// this improves the autogenerated names, allowing them to use derived SI names
if (unit != null && unit.getConversionFactor() == 1)
return unit.getSystem().getBaseUnit(unit.getDimension());
else
return unit;
}
/**
* @return an immutable set of all of the unit names in this database, ignoring prefixes
* @since 2019-01-14
* @since v0.1.0
*/
public Set prefixlessUnitNameSet() {
return Collections.unmodifiableSet(this.units.keySet());
}
/**
* @return an immutable set of all of the prefix names in this database
* @since 2019-01-14
* @since v0.1.0
*/
public Set prefixNameSet() {
return Collections.unmodifiableSet(this.prefixes.keySet());
}
}