summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.org1
-rwxr-xr-xsrc/org/unitConverter/UnitsDatabase.java11
-rw-r--r--src/org/unitConverter/math/DecimalComparison.java107
-rw-r--r--src/org/unitConverter/math/ExpressionParser.java (renamed from src/org/unitConverter/expressionParser/ExpressionParser.java)235
-rw-r--r--src/org/unitConverter/math/package-info.java (renamed from src/org/unitConverter/expressionParser/package-info.java)2
-rwxr-xr-xsrc/org/unitConverter/unit/BaseUnit.java151
-rw-r--r--src/org/unitConverter/unit/LinearUnit.java164
-rw-r--r--src/org/unitConverter/unit/OperatableUnit.java169
-rw-r--r--src/test/java/ExpressionParserTest.java50
-rwxr-xr-xsrc/test/java/UnitTest.java66
10 files changed, 592 insertions, 364 deletions
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 87e26e0..5baf980 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -5,6 +5,7 @@ All notable changes in this project will be shown in this file.
*** Changed
- Moved project to Maven
- Downgraded JUnit to 4.11
+ - BaseUnit is now a subclass of LinearUnit
*** Added
- GUI for a selection-based unit converter
- The UnitDatabase now stores dimensions.
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 4d41735..3af1c8d 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -393,8 +393,6 @@ public final class UnitsDatabase {
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.");
}
@@ -464,7 +462,7 @@ public final class UnitsDatabase {
// parse the expression
// start with an "empty" unit then apply operations on it
- LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
+ 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
@@ -567,8 +565,6 @@ public final class UnitsDatabase {
// 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.");
}
@@ -594,10 +590,7 @@ public final class UnitsDatabase {
// 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();
- }
+ final Unit other = this.getUnit(part);
if (other instanceof LinearUnit) {
if (dividing) {
unit = unit.dividedBy((LinearUnit) other);
diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java
new file mode 100644
index 0000000..e6fb733
--- /dev/null
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (C) 2019 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 org.unitConverter.math;
+
+/**
+ * A class that contains methods to compare float and double values.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-18
+ */
+public final class DecimalComparison {
+ /**
+ * The value used for double comparison. If two double values are within this value multiplied by the larger value,
+ * they are considered equal.
+ *
+ * @since 2019-03-18
+ */
+ public static final double DOUBLE_EPSILON = 1.0e-15;
+
+ /**
+ * The value used for float comparison. If two float values are within this value multiplied by the larger value,
+ * they are considered equal.
+ *
+ * @since 2019-03-18
+ */
+ public static final float FLOAT_EPSILON = 1.0e-6f;
+
+ /**
+ * Tests for equality of double values using {@link #DOUBLE_EPSILON}.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final double a, final double b) {
+ return DecimalComparison.equals(a, b, DOUBLE_EPSILON);
+ }
+
+ /**
+ * Tests for double equality using a custom epsilon value.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @param epsilon
+ * allowed difference
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final double a, final double b, final double epsilon) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ /**
+ * Tests for equality of float values using {@link #FLOAT_EPSILON}.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final float a, final float b) {
+ return DecimalComparison.equals(a, b, FLOAT_EPSILON);
+ }
+
+ /**
+ * Tests for float equality using a custom epsilon value.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @param epsilon
+ * allowed difference
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final float a, final float b, final float epsilon) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ // You may NOT get any DecimalComparison instances
+ private DecimalComparison() {
+ throw new AssertionError();
+ }
+
+}
diff --git a/src/org/unitConverter/expressionParser/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index 804ea87..e06a58b 100644
--- a/src/org/unitConverter/expressionParser/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -14,11 +14,14 @@
* 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 org.unitConverter.expressionParser;
+package org.unitConverter.math;
import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BinaryOperator;
@@ -177,6 +180,9 @@ public final class ExpressionParser<T> {
/**
* Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
*/
@Override
public int compareTo(final PriorityBinaryOperator<T> o) {
@@ -187,6 +193,14 @@ public final class ExpressionParser<T> {
else
return 0;
}
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
}
/**
@@ -217,6 +231,9 @@ public final class ExpressionParser<T> {
/**
* Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
*/
@Override
public int compareTo(final PriorityUnaryOperator<T> o) {
@@ -227,6 +244,14 @@ public final class ExpressionParser<T> {
else
return 0;
}
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
}
/**
@@ -240,6 +265,100 @@ public final class ExpressionParser<T> {
}
/**
+ * The opening bracket.
+ *
+ * @since 2019-03-22
+ */
+ public static final char OPENING_BRACKET = '(';
+
+ /**
+ * The closing bracket.
+ *
+ * @since 2019-03-22
+ */
+ public static final char CLOSING_BRACKET = ')';
+
+ /**
+ * Finds the other bracket in a pair of brackets, given the position of one.
+ *
+ * @param string
+ * string that contains brackets
+ * @param bracketPosition
+ * position of first bracket
+ * @return position of matching bracket
+ * @throws NullPointerException
+ * if string is null
+ * @since 2019-03-22
+ */
+ private static int findBracketPair(final String string, final int bracketPosition) {
+ Objects.requireNonNull(string, "string must not be null.");
+
+ final char openingBracket = string.charAt(bracketPosition);
+
+ // figure out what closing bracket to look for
+ final char closingBracket;
+ switch (openingBracket) {
+ case '(':
+ closingBracket = ')';
+ break;
+ case '[':
+ closingBracket = ']';
+ break;
+ case '{':
+ closingBracket = '}';
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket));
+ }
+
+ // level of brackets. every opening bracket increments this; every closing bracket decrements it
+ int bracketLevel = 0;
+
+ // iterate over the string to find the closing bracket
+ for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) {
+ final char currentCharacter = string.charAt(currentPosition);
+
+ if (currentCharacter == openingBracket) {
+ bracketLevel++;
+ } else if (currentCharacter == closingBracket) {
+ bracketLevel--;
+ if (bracketLevel == 0)
+ return currentPosition;
+ }
+ }
+
+ 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}.
*
@@ -287,14 +406,124 @@ public final class ExpressionParser<T> {
* {@code 2 3 4 + *}.
*
* @param expression
- * @return
+ * expression
+ * @return expression in RPN
* @since 2019-03-17
*/
private String convertExpressionToReversePolish(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
+ final List<String> components = new ArrayList<>();
+
+ // the part of the expression remaining to parse
+ String partialExpression = expression;
+
+ // find and deal with brackets
+ 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);
+ }
+
+ // add everything else
+ components.addAll(Arrays.asList(partialExpression.split(" ")));
+
+ // remove empty entries
+ while (components.contains("")) {
+ components.remove("");
+ }
+
+ // turn the expression into reverse Polish
+ while (true) {
+ final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
+ if (highestPriorityOperatorPosition == -1) {
+ break;
+ }
+
+ switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) {
+ case UNARY_OPERATOR:
+ final String unaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand = components.remove(highestPriorityOperatorPosition);
+ components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator);
+ break;
+ case BINARY_OPERATOR:
+ final String binaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand1 = components.remove(highestPriorityOperatorPosition - 1);
+ final String operand2 = components.remove(highestPriorityOperatorPosition - 1);
+ components.add(highestPriorityOperatorPosition - 1,
+ operand2 + " " + operand1 + " " + binaryOperator);
+ break;
+ default:
+ throw new AssertionError("Expected operator, found non-operator.");
+ }
+ }
+
+ // join all of the components together, then ensure there is only one space in a row
+ String expressionRPN = String.join(" ", components).replaceAll(" +", " ");
+
+ while (expressionRPN.charAt(0) == ' ') {
+ expressionRPN = expressionRPN.substring(1);
+ }
+ while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') {
+ expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
+ }
+ return expressionRPN;
+
// TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
- throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Finds the position of the highest-priority operator in a list
+ *
+ * @param components
+ * components to test
+ * @param blacklist
+ * positions of operators that should be ignored
+ * @return position of highest priority, or -1 if the list contains no operators
+ * @throws NullPointerException
+ * if components is null
+ * @since 2019-03-22
+ */
+ private int findHighestPriorityOperatorPosition(final List<String> components) {
+ Objects.requireNonNull(components, "components must not be null.");
+ // find highest priority
+ int maxPriority = Integer.MIN_VALUE;
+ int maxPriorityPosition = -1;
+
+ // go over components one by one
+ // if it is an operator, test its priority to see if it's max
+ // if it is, update maxPriority and maxPriorityPosition
+ for (int i = 0; i < components.size(); i++) {
+
+ switch (this.getTokenType(components.get(i))) {
+ case UNARY_OPERATOR:
+ final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators.get(components.get(i));
+ final int unaryPriority = unaryOperator.getPriority();
+
+ if (unaryPriority > maxPriority) {
+ maxPriority = unaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ case BINARY_OPERATOR:
+ final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators.get(components.get(i));
+ final int binaryPriority = binaryOperator.getPriority();
+
+ if (binaryPriority > maxPriority) {
+ maxPriority = binaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // max priority position found
+ return maxPriorityPosition;
}
/**
diff --git a/src/org/unitConverter/expressionParser/package-info.java b/src/org/unitConverter/math/package-info.java
index 28f0cae..65d6b23 100644
--- a/src/org/unitConverter/expressionParser/package-info.java
+++ b/src/org/unitConverter/math/package-info.java
@@ -20,4 +20,4 @@
* @author Adrien Hopkins
* @since 2019-03-14
*/
-package org.unitConverter.expressionParser; \ No newline at end of file
+package org.unitConverter.math; \ No newline at end of file
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
index 894d338..2def48e 100755
--- a/src/org/unitConverter/unit/BaseUnit.java
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -28,7 +28,7 @@ import org.unitConverter.dimension.UnitDimension;
* @since 2018-12-23
* @since v0.1.0
*/
-public final class BaseUnit extends AbstractUnit implements OperatableUnit {
+public final class BaseUnit extends LinearUnit {
/**
* Is this unit a full base (i.e. m, s, ... but not N, J, ...)
*
@@ -52,156 +52,65 @@ public final class BaseUnit extends AbstractUnit implements OperatableUnit {
* @since v0.1.0
*/
BaseUnit(final UnitDimension dimension, final UnitSystem system) {
- super(dimension, system);
+ super(dimension, system, 1);
this.isFullBase = dimension.isBase();
}
/**
- * @return this unit as a {@code LinearUnit}
- * @since 2019-01-25
- * @since v0.1.0
- */
- public LinearUnit asLinearUnit() {
- return this.times(1);
- }
-
- @Override
- public double convertFromBase(final double value) {
- return value;
- }
-
- @Override
- public double convertToBase(final double value) {
- return value;
- }
-
- /**
- * Divides this unit by another unit.
+ * Returns the quotient of this unit and another.
+ * <p>
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
*
- * @param other
+ * @param divisor
* unit to divide by
* @return quotient of two units
* @throws IllegalArgumentException
- * if this unit's system is not other's system
+ * if {@code divisor} is not compatible for division as described above
* @throws NullPointerException
- * if other is null
+ * if {@code divisor} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public BaseUnit dividedBy(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for division.");
- return new BaseUnit(this.getDimension().dividedBy(other.getDimension()), this.getSystem());
- }
+ public BaseUnit dividedBy(final BaseUnit divisor) {
+ Objects.requireNonNull(divisor, "other must not be null.");
- /**
- * Divides this unit by a divisor
- *
- * @param divisor
- * amount to divide by
- * @return quotient
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit dividedBy(final double divisor) {
- return new LinearUnit(this, 1 / divisor);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (!(obj instanceof BaseUnit))
- return false;
- final BaseUnit other = (BaseUnit) obj;
- return Objects.equals(this.getSystem(), other.getSystem())
- && Objects.equals(this.getDimension(), other.getDimension());
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = result * prime + this.getSystem().hashCode();
- result = result * prime + this.getDimension().hashCode();
- return result;
- }
-
- @Override
- public LinearUnit negated() {
- return this.times(-1);
- }
-
- @Override
- public OperatableUnit plus(final OperatableUnit addend) {
- Objects.requireNonNull(addend, "addend must not be null.");
-
- // reject addends that cannot be added to this unit
- if (!this.getSystem().equals(addend.getSystem()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
- if (!this.getDimension().equals(addend.getDimension()))
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
-
- // add them together
- if (addend instanceof BaseUnit)
- return this.times(2);
- else
- return addend.plus(this);
- }
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
- @Override
- public BaseUnit reciprocal() {
- return this.toExponent(-1);
+ return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem());
}
/**
- * Multiplies this unit by another unit.
+ * Returns the product of this unit and another.
+ * <p>
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
*
- * @param other
+ * @param multiplier
* unit to multiply by
* @return product of two units
* @throws IllegalArgumentException
- * if this unit's system is not other's system
+ * if {@code multiplier} is not compatible for multiplication as described above
* @throws NullPointerException
- * if other is null
+ * if {@code multiplier} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public BaseUnit times(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for multiplication.");
- return new BaseUnit(this.getDimension().times(other.getDimension()), this.getSystem());
- }
-
- /**
- * Multiplies this unit by a multiplier.
- *
- * @param multiplier
- * amount to multiply by
- * @return product
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit times(final double multiplier) {
- return new LinearUnit(this, multiplier);
- }
+ public BaseUnit times(final BaseUnit multiplier) {
+ Objects.requireNonNull(multiplier, "other must not be null");
- @Override
- public OperatableUnit times(final OperatableUnit multiplier) {
- Objects.requireNonNull(multiplier, "multiplier must not be null.");
-
- // reject multipliers that cannot be muliplied by this unit
+ // check that these units can be multiplied
if (!this.getSystem().equals(multiplier.getSystem()))
- throw new IllegalArgumentException(String
- .format("Incompatible units for multiplication or division \"%s\" and \"%s\".", this, multiplier));
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
// multiply the units
- if (multiplier instanceof BaseUnit)
- return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem());
- else
- return multiplier.times(this);
+ return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem());
}
/**
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
index 64eff1f..c755f79 100644
--- a/src/org/unitConverter/unit/LinearUnit.java
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -19,6 +19,7 @@ package org.unitConverter.unit;
import java.util.Objects;
import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
/**
* A unit that is equal to a certain number multiplied by its base.
@@ -27,7 +28,7 @@ import org.unitConverter.dimension.UnitDimension;
* @since 2018-12-22
* @since v0.1.0
*/
-public final class LinearUnit extends AbstractUnit implements OperatableUnit {
+public class LinearUnit extends AbstractUnit {
/**
* The value of one of this unit in this unit's base unit
*
@@ -91,20 +92,33 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
}
/**
- * Divides this unit by another unit.
+ * Returns the quotient of this unit and another.
+ * <p>
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
*
- * @param other
+ * @param divisor
* unit to divide by
* @return quotient of two units
+ * @throws IllegalArgumentException
+ * if {@code divisor} is not compatible for division as described above
* @throws NullPointerException
- * if other is null
+ * if {@code divisor} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public LinearUnit dividedBy(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().dividedBy(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() / other.getConversionFactor());
+ public LinearUnit dividedBy(final LinearUnit divisor) {
+ Objects.requireNonNull(divisor, "other must not be null");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
+
+ // divide the units
+ final BaseUnit base = this.getBase().dividedBy(divisor.getBase());
+ return new LinearUnit(base, this.getConversionFactor() / divisor.getConversionFactor());
}
@Override
@@ -112,12 +126,13 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
if (!(obj instanceof LinearUnit))
return false;
final LinearUnit other = (LinearUnit) obj;
- return Objects.equals(this.getBase(), other.getBase())
- && Objects.equals(this.getConversionFactor(), other.getConversionFactor());
+ return Objects.equals(this.getSystem(), other.getSystem())
+ && Objects.equals(this.getDimension(), other.getDimension())
+ && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor());
}
/**
- * @return conversionFactor
+ * @return conversion factor between this unit and its base
* @since 2018-12-22
* @since v0.1.0
*/
@@ -129,43 +144,66 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
public int hashCode() {
final int prime = 31;
int result = 1;
- result = result * prime + this.getBase().hashCode();
+ result = result * prime + this.getSystem().hashCode();
+ result = result * prime + this.getDimension().hashCode();
result = result * prime + Double.hashCode(this.getConversionFactor());
return result;
}
- @Override
- public LinearUnit negated() {
- return new LinearUnit(this.getBase(), -this.getConversionFactor());
+ /**
+ * Returns the difference of this unit and another.
+ * <p>
+ * Two units can be subtracted if they have the same base. If {@code subtrahend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ * </p>
+ *
+ * @param subtrahend
+ * unit to subtract
+ * @return difference of units
+ * @throws IllegalArgumentException
+ * if {@code subtrahend} is not compatible for subtraction as described above
+ * @throws NullPointerException
+ * if {@code subtrahend} is null
+ * @since 2019-03-17
+ */
+ public LinearUnit minus(final LinearUnit subtrahendend) {
+ Objects.requireNonNull(subtrahendend, "addend must not be null.");
+
+ // reject subtrahends that cannot be added to this unit
+ if (!this.getBase().equals(subtrahendend.getBase()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend));
+
+ // add the units
+ return new LinearUnit(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor());
}
- @Override
- public OperatableUnit plus(final OperatableUnit addend) {
+ /**
+ * Returns the sum of this unit and another.
+ * <p>
+ * Two units can be added if they have the same base. If {@code addend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ * </p>
+ *
+ * @param addend
+ * unit to add
+ * @return sum of units
+ * @throws IllegalArgumentException
+ * if {@code addend} is not compatible for addition as described above
+ * @throws NullPointerException
+ * if {@code addend} is null
+ * @since 2019-03-17
+ */
+ public LinearUnit plus(final LinearUnit addend) {
Objects.requireNonNull(addend, "addend must not be null.");
// reject addends that cannot be added to this unit
- if (!this.getSystem().equals(addend.getSystem()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
- if (!this.getDimension().equals(addend.getDimension()))
+ if (!this.getBase().equals(addend.getBase()))
throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
+ String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend));
// add the units
- if (addend instanceof BaseUnit)
- // since addend's dimension is equal to this unit's dimension, and there is only one base unit per
- // system-dimension, addend must be this unit's base.
- return new LinearUnit(this.getBase(), this.getConversionFactor() + 1);
- else if (addend instanceof LinearUnit)
- return new LinearUnit(this.getBase(),
- this.getConversionFactor() + ((LinearUnit) addend).getConversionFactor());
- else
- return addend.times(this);
- }
-
- @Override
- public LinearUnit reciprocal() {
- return this.toExponent(-1);
+ return new LinearUnit(this.getBase(), this.getConversionFactor() + addend.getConversionFactor());
}
/**
@@ -182,40 +220,33 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
}
/**
- * Multiplies this unit by another unit.
+ * Returns the product of this unit and another.
+ * <p>
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
*
- * @param other
- * unit to multiply by=
+ * @param multiplier
+ * unit to multiply by
* @return product of two units
+ * @throws IllegalArgumentException
+ * if {@code multiplier} is not compatible for multiplication as described above
* @throws NullPointerException
- * if other is null
+ * if {@code multiplier} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public LinearUnit times(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().times(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() * other.getConversionFactor());
- }
-
- @Override
- public OperatableUnit times(final OperatableUnit multiplier) {
- Objects.requireNonNull(multiplier, "multiplier must not be null.");
+ public LinearUnit times(final LinearUnit multiplier) {
+ Objects.requireNonNull(multiplier, "other must not be null");
- // reject multipliers that cannot be muliplied by this unit
+ // check that these units can be multiplied
if (!this.getSystem().equals(multiplier.getSystem()))
- throw new IllegalArgumentException(String
- .format("Incompatible units for multiplication or division \"%s\" and \"%s\".", this, multiplier));
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
// multiply the units
- if (multiplier instanceof BaseUnit) {
- final BaseUnit newBase = this.getBase().times((BaseUnit) multiplier);
- return new LinearUnit(newBase, this.getConversionFactor());
- } else if (multiplier instanceof LinearUnit) {
- final BaseUnit base = this.getBase().times(multiplier.getBase());
- return new LinearUnit(base, this.getConversionFactor() * ((LinearUnit) multiplier).getConversionFactor());
- } else
- return multiplier.times(this);
+ final BaseUnit base = this.getBase().times(multiplier.getBase());
+ return new LinearUnit(base, this.getConversionFactor() * multiplier.getConversionFactor());
}
/**
@@ -227,7 +258,6 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
* @since 2019-01-15
* @since v0.1.0
*/
- @Override
public LinearUnit toExponent(final int exponent) {
return new LinearUnit(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent));
}
@@ -236,4 +266,16 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
public String toString() {
return super.toString() + String.format(" (equal to %s * base)", this.getConversionFactor());
}
+
+ /**
+ * Returns the result of applying {@code prefix} to this unit.
+ *
+ * @param prefix
+ * prefix to apply
+ * @return unit with prefix
+ * @since 2019-03-18
+ */
+ public LinearUnit withPrefix(final UnitPrefix prefix) {
+ return this.times(prefix.getMultiplier());
+ }
}
diff --git a/src/org/unitConverter/unit/OperatableUnit.java b/src/org/unitConverter/unit/OperatableUnit.java
deleted file mode 100644
index ae11c41..0000000
--- a/src/org/unitConverter/unit/OperatableUnit.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * Copyright (C) 2019 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 org.unitConverter.unit;
-
-/**
- * A unit that can be added, subtracted, multiplied or divided by another operatable unit, and raised to an integer
- * exponent.
- * <p>
- * In order to use two units in an operation, they must be part of the same unit system. In addition, in order for two
- * units to add or subtract, they must measure the same dimension.
- * </p>
- * <p>
- * It is okay for an operation to throw a {@code ClassCastException} if the operator's class cannot operate with another
- * class. However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- * </p>
- *
- * @author Adrien Hopkins
- * @since 2019-03-17
- */
-public interface OperatableUnit extends Unit {
- /**
- * Returns the quotient of this unit and another.
- * <p>
- * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
- * condition, an {@code IllegalArgumentException} should be thrown.
- * </p>
- * <p>
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code divisor}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- * </p>
- *
- * @param divisor
- * unit to divide by
- * @return quotient
- * @throws IllegalArgumentException
- * if {@code divisor} is not compatible for division as described above
- * @throws NullPointerException
- * if {@code divisor} is null
- * @throws ClassCastException
- * if {@code divisor}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- default OperatableUnit dividedBy(final OperatableUnit divisor) {
- return this.times(divisor.reciprocal());
- }
-
- /**
- * Returns the difference of this unit and another.
- * <p>
- * Two units can be subtracted if they meet the following conditions:
- * <ul>
- * <li>The two units are part of the same UnitSystem.</li>
- * <li>The two units have the same {@code dimension}.</li>
- * </ul>
- * If {@code subtrahend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown.
- * </p>
- * <p>
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code subtrahend}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- * </p>
- *
- * @param subtrahend
- * unit to subtract
- * @return difference
- * @throws IllegalArgumentException
- * if {@code subtrahend} is not compatible for subtraction as described above
- * @throws NullPointerException
- * if {@code subtrahend} is null
- * @throws ClassCastException
- * if {@code subtrahend}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- default OperatableUnit minus(final OperatableUnit subtrahend) {
- return this.plus(subtrahend.negated());
- }
-
- /**
- * @return this unit negated, i.e. -this
- * @since 2019-03-17
- */
- OperatableUnit negated();
-
- /**
- * Returns the sum of this unit and another.
- * <p>
- * Two units can be added if they meet the following conditions:
- * <ul>
- * <li>The two units are part of the same UnitSystem.</li>
- * <li>The two units have the same {@code dimension}.</li>
- * </ul>
- * If {@code addend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown.
- * </p>
- * <p>
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code addend}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- * </p>
- *
- * @param addend
- * unit to add
- * @return sum
- * @throws IllegalArgumentException
- * if {@code addend} is not compatible for addition as described above
- * @throws NullPointerException
- * if {@code addend} is null
- * @throws ClassCastException
- * if {@code addend}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- OperatableUnit plus(OperatableUnit addend);
-
- /**
- * @return reciprocal of this unit
- * @since 2019-03-17
- */
- OperatableUnit reciprocal();
-
- /**
- * Returns the product of this unit and another.
- * <p>
- * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
- * condition, an {@code IllegalArgumentException} should be thrown.
- * </p>
- * <p>
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code multiplier}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- * </p>
- *
- * @param multiplier
- * unit to multiply by
- * @return product
- * @throws IllegalArgumentException
- * if {@code multiplier} is not compatible for multiplication as described above
- * @throws NullPointerException
- * if {@code multiplier} is null
- * @throws ClassCastException
- * if {@code multiplier}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- OperatableUnit times(OperatableUnit multiplier);
-
- /**
- * Returns the result of raising this unit to the exponent {@code exponent}.
- *
- * @param exponent
- * exponent to exponentiate by
- * @return result of exponentiation
- * @since 2019-03-17
- */
- OperatableUnit toExponent(int exponent);
-}
diff --git a/src/test/java/ExpressionParserTest.java b/src/test/java/ExpressionParserTest.java
new file mode 100644
index 0000000..e81ca40
--- /dev/null
+++ b/src/test/java/ExpressionParserTest.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 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 test.java;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.unitConverter.math.ExpressionParser;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-03-22
+ */
+public class ExpressionParserTest {
+ private static final ExpressionParser<Integer> numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
+ .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("-", (o1, o2) -> o1 - o2, 0)
+ .addBinaryOperator("*", (o1, o2) -> o1 * o2, 1).addBinaryOperator("/", (o1, o2) -> o1 / o2, 1)
+ .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
+
+ /**
+ * Test method for {@link org.unitConverter.math.ExpressionParser#parseExpression(java.lang.String)}.
+ */
+ @Test
+ public void testParseExpression() {
+ // test parsing of expressions
+ assertEquals((int) numberParser.parseExpression("1 + 2 ^ 5 * 3"), 97);
+ assertEquals((int) numberParser.parseExpression("(1 + 2) ^ 5 * 3"), 729);
+
+ // ensure it normally goes left to right
+ assertEquals((int) numberParser.parseExpression("1 + 2 + 3 + 4"), 10);
+ assertEquals((int) numberParser.parseExpression("12 - 4 - 3"), 5);
+ assertEquals((int) numberParser.parseExpression("12 - (4 - 3)"), 11);
+ assertEquals((int) numberParser.parseExpression("1 / 2 + 3"), 3);
+ }
+
+}
diff --git a/src/test/java/UnitTest.java b/src/test/java/UnitTest.java
index 45f890f..79bc3d1 100755
--- a/src/test/java/UnitTest.java
+++ b/src/test/java/UnitTest.java
@@ -18,10 +18,16 @@ package test.java;
import static org.junit.Assert.assertEquals;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
import org.junit.Test;
import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.math.DecimalComparison;
import org.unitConverter.unit.BaseUnit;
+import org.unitConverter.unit.LinearUnit;
import org.unitConverter.unit.SI;
+import org.unitConverter.unit.SIPrefix;
import org.unitConverter.unit.Unit;
/**
@@ -31,12 +37,46 @@ import org.unitConverter.unit.Unit;
* @since 2018-12-22
*/
public class UnitTest {
+ /** A random number generator */
+ private static final Random rng = ThreadLocalRandom.current();
+
+ @Test
+ public void testAdditionAndSubtraction() {
+ final LinearUnit inch = SI.METRE.times(0.0254);
+ final LinearUnit foot = SI.METRE.times(0.3048);
+
+ assertEquals(inch.plus(foot), SI.METRE.times(0.3302));
+ assertEquals(foot.minus(inch), SI.METRE.times(0.2794));
+ }
+
+ @Test
+ public void testBaseUnitExclusives() {
+ // this test should have a compile error if I am doing something wrong
+ final BaseUnit metrePerSecondSquared = SI.METRE.dividedBy(SI.SECOND.toExponent(2));
+
+ assertEquals(metrePerSecondSquared, SI.SI.getBaseUnit(StandardDimensions.ACCELERATION));
+ }
+
@Test
public void testConversion() {
final BaseUnit metre = SI.METRE;
final Unit inch = metre.times(0.0254);
assertEquals(1.9, inch.convertToBase(75), 0.01);
+
+ // try random stuff
+ for (int i = 0; i < 1000; i++) {
+ // initiate random values
+ final double conversionFactor = rng.nextDouble() * 1000000;
+ final double testValue = rng.nextDouble() * 1000000;
+ final double expected = testValue * conversionFactor;
+
+ // test
+ final Unit unit = SI.METRE.times(conversionFactor);
+ final double actual = unit.convertToBase(testValue);
+
+ assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON);
+ }
}
@Test
@@ -46,4 +86,30 @@ public class UnitTest {
assertEquals(metre, meter);
}
+
+ @Test
+ public void testMultiplicationAndDivision() {
+ // test unit-times-unit multiplication
+ final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ final LinearUnit actualJoule = SI.SI.getBaseUnit(StandardDimensions.ENERGY);
+
+ assertEquals(generatedJoule, actualJoule);
+
+ // test multiplication by conversion factors
+ final LinearUnit kilometre = SI.METRE.times(1000);
+ final LinearUnit hour = SI.SECOND.times(3600);
+ final LinearUnit generatedKPH = kilometre.dividedBy(hour);
+
+ final LinearUnit actualKPH = SI.SI.getBaseUnit(StandardDimensions.VELOCITY).dividedBy(3.6);
+
+ assertEquals(generatedKPH, actualKPH);
+ }
+
+ @Test
+ public void testPrefixes() {
+ final LinearUnit generatedKilometre = SI.METRE.withPrefix(SIPrefix.KILO);
+ final LinearUnit actualKilometre = SI.METRE.times(1000);
+
+ assertEquals(generatedKilometre, actualKilometre);
+ }
}