summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-03-22 19:35:30 -0400
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-03-22 19:35:30 -0400
commitbfe1f266922bffd3c0c8d8906535be7621217e7a (patch)
tree3e85bac0398208db251f7365aa479528409f04f3
parent943496888d18b031be19ba8e7348ec188dc8eb6b (diff)
Unit Expressions are now parsed with the expression parser.
Addition and subtraction are now possible.
-rw-r--r--CHANGELOG.org1
-rwxr-xr-xsrc/org/unitConverter/UnitsDatabase.java326
-rwxr-xr-xsrc/org/unitConverter/converterGUI/UnitConverterGUI.java7
-rw-r--r--src/org/unitConverter/math/ExpressionParser.java108
-rwxr-xr-xunitsfile.txt2
5 files changed, 213 insertions, 231 deletions
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 5baf980..95dc57a 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -10,6 +10,7 @@ All notable changes in this project will be shown in this file.
- GUI for a selection-based unit converter
- The UnitDatabase now stores dimensions.
- A system to parse mathematical expressions, used to parse unit expressions.
+ - You can now add and subtract in unit expressions!
** v0.1.0
NOTE: At this stage, the API is subject to significant change.
*** Added
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 3af1c8d..481ce93 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -21,13 +21,17 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
+import org.unitConverter.math.ExpressionParser;
import org.unitConverter.unit.AbstractUnit;
import org.unitConverter.unit.BaseUnit;
import org.unitConverter.unit.DefaultUnitPrefix;
@@ -68,6 +72,32 @@ public final class UnitsDatabase {
private final Map<String, UnitDimension> dimensions;
/**
+ * A parser that can parse unit expressions.
+ *
+ * @since 2019-03-22
+ */
+ private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>(
+ this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
+ .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1).addBinaryOperator("^", (o1, o2) -> {
+ // exponent function - first check if o2 is a number,
+ if (o2.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
+ // then check if it is an integer,
+ final double exponent = o2.getConversionFactor();
+ if (DecimalComparison.equals(exponent % 1, 0))
+ // then exponentiate
+ return o1.toExponent((int) (exponent % 1 + 0.5));
+ else
+ // not an integer
+ throw new UnsupportedOperationException(
+ "Decimal exponents are currently not supported.");
+ } else
+ // not a number
+ throw new IllegalArgumentException("Exponents must be numbers.");
+ }, 2).build();
+
+ /**
* Creates the {@code UnitsDatabase}.
*
* @since 2019-01-10
@@ -299,6 +329,37 @@ public final class UnitsDatabase {
}
/**
+ * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an
+ * {@code IllegalArgumentException}.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-03-22
+ */
+ private LinearUnit getLinearUnit(final String name) {
+ // see if I am using a function-unit like tempC(100)
+ if (name.contains("(") && name.contains(")")) {
+ // break it into function name and value
+ final List<String> parts = Arrays.asList(name.split("\\("));
+ if (parts.size() != 2)
+ throw new IllegalArgumentException("Format nonlinear units like: unit(value).");
+
+ // solve the function
+ final Unit unit = this.getUnit(parts.get(0));
+ final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1));
+ return unit.getBase().times(unit.convertToBase(value));
+ } else {
+ // get a linear unit
+ final Unit unit = this.getUnit(name);
+ if (unit instanceof LinearUnit)
+ return (LinearUnit) unit;
+ else
+ throw new IllegalArgumentException(String.format("%s is not a linear unit.", name));
+ }
+ }
+
+ /**
* Gets a unit prefix from the database from its name
*
* @param name
@@ -383,55 +444,60 @@ public final class UnitsDatabase {
* @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
- throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
- }
+ try {
+ final double value = Double.parseDouble(name);
+ return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value);
+ } catch (final NumberFormatException e) {
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
+ 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
+ throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
+ }
- 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());
+ 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);
}
- return this.units.get(name);
}
}
@@ -453,165 +519,39 @@ public final class UnitsDatabase {
* @throws IllegalArgumentException
* if the expression cannot be parsed
* @throws NullPointerException
- * if any argument is null
+ * if expression 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);
- 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
- 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 + "\".");
- final Unit other = this.getUnit(part);
- 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).");
- }
- }
+ // attempt to get a unit as an alias first
+ if (this.containsUnitName(expression))
+ return this.getUnit(expression);
+
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
+ modifiedExpression = modifiedExpression.replaceAll("-", " - ");
+ modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
+ modifiedExpression = modifiedExpression.replaceAll("/", " / ");
+ modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
+
+ // fix broken spaces
+ modifiedExpression = modifiedExpression.replaceAll(" +", " ");
+
+ // the previous operation breaks negative numbers, fix them!
+ // (i.e. -2 becomes - 2)
+ for (int i = 2; i < modifiedExpression.length(); i++) {
+ if (modifiedExpression.charAt(i) == '-'
+ && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) {
+ // found a broken negative number
+ modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2);
}
}
- // 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 this.unitExpressionParser.parseExpression(modifiedExpression);
}
/**
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index a70e971..867211c 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -175,7 +175,12 @@ final class UnitConverterGUI {
// try to parse to
final Unit to;
try {
- to = this.units.getUnitFromExpression(toUnitString);
+ // if it's a unit, convert to that
+ if (this.units.containsUnitName(toUnitString)) {
+ to = this.units.getUnit(toUnitString);
+ } else {
+ to = this.units.getUnitFromExpression(toUnitString);
+ }
} catch (final IllegalArgumentException e) {
this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage());
return;
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index e06a58b..f34a0c2 100644
--- a/src/org/unitConverter/math/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -56,6 +56,13 @@ public final class ExpressionParser<T> {
private final Function<String, T> objectObtainer;
/**
+ * The function of the space as an operator (like 3 x y)
+ *
+ * @since 2019-03-22
+ */
+ private String spaceFunction = null;
+
+ /**
* A map mapping operator strings to operator functions, for unary operators.
*
* @since 2019-03-14
@@ -115,6 +122,24 @@ public final class ExpressionParser<T> {
}
/**
+ * Adds a function for spaces. You must use the text of an existing binary operator.
+ *
+ * @param operator
+ * text of operator to use
+ * @return this builder
+ * @since 2019-03-22
+ */
+ public Builder<T> addSpaceFunction(final String operator) {
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ if (!this.binaryOperators.containsKey(operator))
+ throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator));
+
+ this.spaceFunction = operator;
+ return this;
+ }
+
+ /**
* Adds a unary operator to the builder.
*
* @param text
@@ -148,7 +173,8 @@ public final class ExpressionParser<T> {
* @since 2019-03-17
*/
public ExpressionParser<T> build() {
- return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators);
+ return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators,
+ this.spaceFunction);
}
}
@@ -330,34 +356,6 @@ public final class ExpressionParser<T> {
throw new IllegalArgumentException("No matching bracket found.");
}
- public static void main(final String[] args) {
- final ExpressionParser<Integer> numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
- .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("*", (o1, o2) -> o1 * o2, 1)
- .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
- System.out.println(numberParser.convertExpressionToReversePolish("(1 + 2) ^ 5 * 3"));
- System.out.println(numberParser.parseExpression("(1 + 2) ^ 5 * 3")); // 729
- }
-
- /**
- * Swaps two elements in a list. Modifies the list passed in instead of returning a modified list.
- *
- * @param list
- * list to swap elements
- * @param firstIndex
- * index of first element to swap
- * @param otherIndex
- * index of other element to swap
- * @throws NullPointerException
- * if list is null
- * @since 2019-03-20
- */
- private static <E> void swap(final List<E> list, final int firstIndex, final int otherIndex) {
- Objects.requireNonNull(list, "list must not be null.");
- final E temp = list.get(firstIndex);
- list.set(firstIndex, list.get(otherIndex));
- list.set(otherIndex, temp);
- }
-
/**
* A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would
* use {@code Integer::parseInt}.
@@ -381,20 +379,32 @@ public final class ExpressionParser<T> {
private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
/**
+ * The operator for space, or null if spaces have no function.
+ *
+ * @since 2019-03-22
+ */
+ private final String spaceOperator;
+
+ /**
* Creates the {@code ExpressionParser}.
*
* @param objectObtainer
* function to get objects from strings
* @param unaryOperators
- * operators available to the parser
+ * unary operators available to the parser
+ * @param binaryOperators
+ * binary operators available to the parser
+ * @param spaceOperator
+ * operator used by spaces
* @since 2019-03-14
*/
private ExpressionParser(final Function<String, T> objectObtainer,
final Map<String, PriorityUnaryOperator<T>> unaryOperators,
- final Map<String, PriorityBinaryOperator<T>> binaryOperators) {
+ final Map<String, PriorityBinaryOperator<T>> binaryOperators, final String spaceOperator) {
this.objectObtainer = objectObtainer;
this.unaryOperators = unaryOperators;
this.binaryOperators = binaryOperators;
+ this.spaceOperator = spaceOperator;
}
/**
@@ -422,10 +432,26 @@ public final class ExpressionParser<T> {
while (partialExpression.indexOf(OPENING_BRACKET) != -1) {
final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET);
final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition);
- components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
- components.add(this.convertExpressionToReversePolish(
- partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
- partialExpression = partialExpression.substring(closingBracketPosition + 1);
+
+ // check for function
+ if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') {
+ // function like sin(2) or tempF(32)
+ // find the position of the last space
+ int spacePosition = openingBracketPosition;
+ while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') {
+ spacePosition--;
+ }
+ // then split the function into pre-function and function, using the space position
+ components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" ")));
+ components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ } else {
+ // normal brackets like (1 + 2) * (3 / 5)
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ }
}
// add everything else
@@ -436,6 +462,16 @@ public final class ExpressionParser<T> {
components.remove("");
}
+ // deal with space multiplication (x y)
+ if (this.spaceOperator != null) {
+ for (int i = 0; i < components.size() - 1; i++) {
+ if (this.getTokenType(components.get(i)) == TokenType.OBJECT
+ && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) {
+ components.add(++i, this.spaceOperator);
+ }
+ }
+ }
+
// turn the expression into reverse Polish
while (true) {
final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
@@ -472,7 +508,7 @@ public final class ExpressionParser<T> {
}
return expressionRPN;
- // TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
+ // TODO document org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
}
/**
diff --git a/unitsfile.txt b/unitsfile.txt
index 78e51f7..2455c8a 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -136,7 +136,7 @@ steradian m^2 / m^2
sr steradian
degree 360 / tau * radian
deg degree
-° degree
+° degree
# Nonlinear units, which are not supported by the file reader and must be defined manually
# Use tempC(100) for 100 degrees Celsius