summaryrefslogtreecommitdiff
path: root/src/unitConverter/UnitsDatabase.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/unitConverter/UnitsDatabase.java')
-rwxr-xr-xsrc/unitConverter/UnitsDatabase.java573
1 files changed, 573 insertions, 0 deletions
diff --git a/src/unitConverter/UnitsDatabase.java b/src/unitConverter/UnitsDatabase.java
new file mode 100755
index 0000000..d479917
--- /dev/null
+++ b/src/unitConverter/UnitsDatabase.java
@@ -0,0 +1,573 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package unitConverter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import unitConverter.dimension.UnitDimension;
+import unitConverter.unit.AbstractUnit;
+import unitConverter.unit.BaseUnit;
+import unitConverter.unit.DefaultUnitPrefix;
+import unitConverter.unit.LinearUnit;
+import unitConverter.unit.SI;
+import unitConverter.unit.Unit;
+import unitConverter.unit.UnitPrefix;
+
+/**
+ * A database of units.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-07
+ */
+public final class UnitsDatabase {
+ /**
+ * The units in this system.
+ *
+ * @since 2019-01-07
+ */
+ private final Map<String, Unit> units;
+
+ /**
+ * The unit prefixes in this system.
+ *
+ * @since 2019-01-14
+ */
+ private final Map<String, UnitPrefix> prefixes;
+
+ /**
+ * Creates the {@code UnitsDatabase}.
+ *
+ * @since 2019-01-10
+ */
+ public UnitsDatabase() {
+ this.units = new HashMap<>();
+ this.prefixes = new HashMap<>();
+ }
+
+ /**
+ * Adds all units from a file, using data from the database to parse them.
+ * <p>
+ * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by
+ * any number of tab characters.
+ * <p>
+ * <p>
+ * Allowed exceptions:
+ * <ul>
+ * <li>Any line that begins with the '#' character is considered a comment and ignored.</li>
+ * <li>Blank lines are also ignored</li>
+ * <li>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.</li>
+ * </ul>
+ *
+ * @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
+ */
+ 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, name.substring(0, name.length() - 1));
+ } 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 prefix to the database using a custom name
+ *
+ * @param name
+ * prefix's name
+ * @param prefix
+ * prefix to add
+ * @throws NullPointerException
+ * if name or unit is null
+ * @since 2019-01-14
+ */
+ 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 using a custom name
+ *
+ * @param name
+ * unit's name
+ * @param unit
+ * unit to add
+ * @throws NullPointerException
+ * if unit is null
+ * @since 2019-01-10
+ */
+ public void addUnit(final String name, final Unit unit) {
+ this.units.put(name, Objects.requireNonNull(unit, "unit must not be null."));
+ }
+
+ /**
+ * 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
+ */
+ 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
+ */
+ 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
+ */
+ 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);
+ }
+
+ /**
+ * Gets a unit prefix from the database from its name
+ *
+ * @param name
+ * prefix's name
+ * @return prefix
+ * @since 2019-01-10
+ */
+ public UnitPrefix getPrefix(final String name) {
+ return this.prefixes.get(name);
+ }
+
+ /**
+ * Gets a unit prefix from a prefix expression and a name
+ * <p>
+ * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of
+ * another prefix
+ * </p>
+ *
+ * @param expression
+ * expression to input
+ * @param name
+ * name of prefix if a new prefix is created
+ * @return prefix
+ * @throws IllegalArgumentException
+ * if expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-14
+ */
+ public UnitPrefix getPrefixFromExpression(final String expression, final String name) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+ Objects.requireNonNull(name, "name 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
+ */
+ 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
+ */
+ 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
+ * <p>
+ * The expression is a series of any of the following:
+ * <ul>
+ * <li>The name of a unit, which multiplies or divides the result based on preceding operators</li>
+ * <li>The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each
+ * other is equivalent to multiplication)</li>
+ * <li>A number which is multiplied or divided</li>
+ * </ul>
+ * This method only works with linear units.
+ * <p>
+ * If the expression contains just the name of a unit, returns that unit without changing name or symbol. This
+ * exists for the creation of aliases.
+ * </p>
+ *
+ * @param line
+ * line to parse
+ * @throws IllegalArgumentException
+ * if the expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-07
+ */
+ 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
+ */
+ public Set<String> prefixlessUnitNameSet() {
+ return Collections.unmodifiableSet(this.units.keySet());
+ }
+
+ /**
+ * @return an immutable set of all of the prefix names in this database
+ * @since 2019-01-14
+ */
+ public Set<String> prefixNameSet() {
+ return Collections.unmodifiableSet(this.prefixes.keySet());
+ }
+}